diff --git a/webrtc/base/BUILD.gn b/webrtc/base/BUILD.gn index 4c344fd7f9..4c2b54c24b 100644 --- a/webrtc/base/BUILD.gn +++ b/webrtc/base/BUILD.gn @@ -288,6 +288,8 @@ static_library("rtc_base") { "ratelimiter.h", "rtccertificate.cc", "rtccertificate.h", + "rtccertificategenerator.cc", + "rtccertificategenerator.h", "sha1.cc", "sha1.h", "sha1digest.cc", diff --git a/webrtc/base/base.gyp b/webrtc/base/base.gyp index 52f2ffe6db..3e89991fb8 100644 --- a/webrtc/base/base.gyp +++ b/webrtc/base/base.gyp @@ -263,6 +263,8 @@ 'ratelimiter.h', 'rtccertificate.cc', 'rtccertificate.h', + 'rtccertificategenerator.cc', + 'rtccertificategenerator.h', 'sha1.cc', 'sha1.h', 'sha1digest.cc', diff --git a/webrtc/base/base_tests.gyp b/webrtc/base/base_tests.gyp index ec389333d5..73eeb0dbaa 100644 --- a/webrtc/base/base_tests.gyp +++ b/webrtc/base/base_tests.gyp @@ -94,6 +94,7 @@ 'referencecountedsingletonfactory_unittest.cc', 'rollingaccumulator_unittest.cc', 'rtccertificate_unittests.cc', + 'rtccertificategenerator_unittest.cc', 'scopedptrcollection_unittest.cc', 'sha1digest_unittest.cc', 'sharedexclusivelock_unittest.cc', diff --git a/webrtc/base/rtccertificategenerator.cc b/webrtc/base/rtccertificategenerator.cc new file mode 100644 index 0000000000..d4572b0159 --- /dev/null +++ b/webrtc/base/rtccertificategenerator.cc @@ -0,0 +1,157 @@ +/* + * Copyright 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "webrtc/base/rtccertificategenerator.h" + +#include + +#include "webrtc/base/checks.h" +#include "webrtc/base/sslidentity.h" + +namespace rtc { + +namespace { + +// A certificates' subject and issuer name. +const char kIdentityName[] = "WebRTC"; + +uint64_t kYearInSeconds = 365 * 24 * 60 * 60; + +enum { + MSG_GENERATE, + MSG_GENERATE_DONE, +}; + +// Helper class for generating certificates asynchronously; a single task +// instance is responsible for a single asynchronous certificate generation +// request. We are using a separate helper class so that a generation request +// can outlive the |RTCCertificateGenerator| that spawned it. +class RTCCertificateGenerationTask : public RefCountInterface, + public MessageHandler { + public: + RTCCertificateGenerationTask( + Thread* signaling_thread, + Thread* worker_thread, + const KeyParams& key_params, + const Optional& expires_ms, + const scoped_refptr& callback) + : signaling_thread_(signaling_thread), + worker_thread_(worker_thread), + key_params_(key_params), + expires_ms_(expires_ms), + callback_(callback) { + RTC_DCHECK(signaling_thread_); + RTC_DCHECK(worker_thread_); + RTC_DCHECK(callback_); + } + ~RTCCertificateGenerationTask() override {} + + // Handles |MSG_GENERATE| and its follow-up |MSG_GENERATE_DONE|. + void OnMessage(Message* msg) override { + switch (msg->message_id) { + case MSG_GENERATE: + RTC_DCHECK(worker_thread_->IsCurrent()); + + // Perform the certificate generation work here on the worker thread. + certificate_ = RTCCertificateGenerator::GenerateCertificate( + key_params_, expires_ms_); + + // Handle callbacks on signaling thread. Pass on the |msg->pdata| + // (which references |this| with ref counting) to that thread. + signaling_thread_->Post(this, MSG_GENERATE_DONE, msg->pdata); + break; + case MSG_GENERATE_DONE: + RTC_DCHECK(signaling_thread_->IsCurrent()); + + // Perform callback with result here on the signaling thread. + if (certificate_) { + callback_->OnSuccess(certificate_); + } else { + callback_->OnFailure(); + } + + // Destroy |msg->pdata| which references |this| with ref counting. This + // may result in |this| being deleted - do not touch member variables + // after this line. + delete msg->pdata; + return; + default: + RTC_NOTREACHED(); + } + } + + private: + Thread* const signaling_thread_; + Thread* const worker_thread_; + const KeyParams key_params_; + const Optional expires_ms_; + const scoped_refptr callback_; + scoped_refptr certificate_; +}; + +} // namespace + +// static +scoped_refptr +RTCCertificateGenerator::GenerateCertificate( + const KeyParams& key_params, + const Optional& expires_ms) { + if (!key_params.IsValid()) + return nullptr; + SSLIdentity* identity; + if (!expires_ms) { + identity = SSLIdentity::Generate(kIdentityName, key_params); + } else { + uint64_t expires_s = *expires_ms / 1000; + // Limit the expiration time to something reasonable (a year). This was + // somewhat arbitrarily chosen. It also ensures that the value is not too + // large for the unspecified |time_t|. + expires_s = std::min(expires_s, kYearInSeconds); + // TODO(torbjorng): Stop using |time_t|, its type is unspecified. It it safe + // to assume it can hold up to a year's worth of seconds (and more), but + // |SSLIdentity::Generate| should stop relying on |time_t|. + // See bugs.webrtc.org/5720. + time_t cert_lifetime_s = static_cast(expires_s); + identity = SSLIdentity::GenerateWithExpiration( + kIdentityName, key_params, cert_lifetime_s); + } + if (!identity) + return nullptr; + scoped_ptr identity_sptr(identity); + return RTCCertificate::Create(std::move(identity_sptr)); +} + +RTCCertificateGenerator::RTCCertificateGenerator( + Thread* signaling_thread, Thread* worker_thread) + : signaling_thread_(signaling_thread), + worker_thread_(worker_thread) { + RTC_DCHECK(signaling_thread_); + RTC_DCHECK(worker_thread_); +} + +void RTCCertificateGenerator::GenerateCertificateAsync( + const KeyParams& key_params, + const Optional& expires_ms, + const scoped_refptr& callback) { + RTC_DCHECK(signaling_thread_->IsCurrent()); + RTC_DCHECK(callback); + + // Create a new |RTCCertificateGenerationTask| for this generation request. It + // is reference counted and referenced by the message data, ensuring it lives + // until the task has completed (independent of |RTCCertificateGenerator|). + ScopedRefMessageData* msg_data = + new ScopedRefMessageData( + new RefCountedObject( + signaling_thread_, worker_thread_, key_params, expires_ms, + callback)); + worker_thread_->Post(msg_data->data().get(), MSG_GENERATE, msg_data); +} + +} // namespace rtc diff --git a/webrtc/base/rtccertificategenerator.h b/webrtc/base/rtccertificategenerator.h new file mode 100644 index 0000000000..08fe67108d --- /dev/null +++ b/webrtc/base/rtccertificategenerator.h @@ -0,0 +1,69 @@ +/* + * Copyright 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef WEBRTC_BASE_RTCCERTIFICATEGENERATOR_H_ +#define WEBRTC_BASE_RTCCERTIFICATEGENERATOR_H_ + +#include "webrtc/base/optional.h" +#include "webrtc/base/refcount.h" +#include "webrtc/base/rtccertificate.h" +#include "webrtc/base/scoped_ref_ptr.h" +#include "webrtc/base/sslidentity.h" +#include "webrtc/base/thread.h" + +namespace rtc { + +class RTCCertificateGeneratorCallback : public RefCountInterface { + public: + virtual void OnSuccess( + const scoped_refptr& certificate) = 0; + virtual void OnFailure() = 0; + + protected: + ~RTCCertificateGeneratorCallback() override {} +}; + +// Generates |RTCCertificate|s. +// The static function |GenerateCertificate| generates a certificate on the +// current thread. The |RTCCertificateGenerator| instance generates certificates +// asynchronously on the worker thread with |GenerateCertificateAsync|. +class RTCCertificateGenerator { + public: + // Generates a certificate on the current thread. Returns null on failure. + // If |expires_ms| is specified, the certificate will expire in approximately + // that many milliseconds from now. |expires_ms| is limited to a year, a + // larger value than that is clamped down to a year. If |expires_ms| is not + // specified, a default expiration time is used. + static scoped_refptr GenerateCertificate( + const KeyParams& key_params, + const Optional& expires_ms); + + RTCCertificateGenerator(Thread* signaling_thread, Thread* worker_thread); + + // Generates a certificate asynchronously on the worker thread. + // Must be called on the signaling thread. The |callback| is invoked with the + // result on the signaling thread. If |expires_ms| is specified, the + // certificate will expire in approximately that many milliseconds from now. + // |expires_ms| is limited to a year, a larger value than that is clamped down + // to a year. If |expires_ms| is not specified, a default expiration time is + // used. + void GenerateCertificateAsync( + const KeyParams& key_params, + const Optional& expires_ms, + const scoped_refptr& callback); + + private: + Thread* const signaling_thread_; + Thread* const worker_thread_; +}; + +} // namespace rtc + +#endif // WEBRTC_BASE_RTCCERTIFICATEGENERATOR_H_ diff --git a/webrtc/base/rtccertificategenerator_unittest.cc b/webrtc/base/rtccertificategenerator_unittest.cc new file mode 100644 index 0000000000..162a2d5b09 --- /dev/null +++ b/webrtc/base/rtccertificategenerator_unittest.cc @@ -0,0 +1,151 @@ +/* + * Copyright 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "webrtc/base/rtccertificategenerator.h" + +#include "webrtc/base/checks.h" +#include "webrtc/base/gunit.h" +#include "webrtc/base/logging.h" +#include "webrtc/base/optional.h" +#include "webrtc/base/scoped_ptr.h" +#include "webrtc/base/thread.h" + +namespace rtc { + +class RTCCertificateGeneratorFixture : public RTCCertificateGeneratorCallback { + public: + RTCCertificateGeneratorFixture() + : signaling_thread_(Thread::Current()), + worker_thread_(new Thread()), + generate_async_completed_(false) { + RTC_CHECK(signaling_thread_); + RTC_CHECK(worker_thread_->Start()); + generator_.reset( + new RTCCertificateGenerator(signaling_thread_, worker_thread_.get())); + } + ~RTCCertificateGeneratorFixture() override {} + + RTCCertificateGenerator* generator() const { return generator_.get(); } + RTCCertificate* certificate() const { return certificate_.get(); } + + void OnSuccess(const scoped_refptr& certificate) { + RTC_CHECK(signaling_thread_->IsCurrent()); + RTC_CHECK(certificate); + certificate_ = certificate; + generate_async_completed_ = true; + } + void OnFailure() { + RTC_CHECK(signaling_thread_->IsCurrent()); + certificate_ = nullptr; + generate_async_completed_ = true; + } + + bool GenerateAsyncCompleted() { + RTC_CHECK(signaling_thread_->IsCurrent()); + if (generate_async_completed_) { + // Reset flag so that future generation requests are not considered done. + generate_async_completed_ = false; + return true; + } + return false; + } + + protected: + Thread* const signaling_thread_; + scoped_ptr worker_thread_; + scoped_ptr generator_; + scoped_refptr certificate_; + bool generate_async_completed_; +}; + +class RTCCertificateGeneratorTest + : public testing::Test { + public: + RTCCertificateGeneratorTest() + : fixture_(new RefCountedObject()) {} + ~RTCCertificateGeneratorTest() {} + + protected: + static const int kGenerationTimeoutMs = 1000; + + scoped_refptr fixture_; +}; + +TEST_F(RTCCertificateGeneratorTest, GenerateECDSA) { + EXPECT_TRUE(RTCCertificateGenerator::GenerateCertificate( + KeyParams::ECDSA(), + Optional())); +} + +TEST_F(RTCCertificateGeneratorTest, GenerateRSA) { + EXPECT_TRUE(RTCCertificateGenerator::GenerateCertificate( + KeyParams::RSA(), + Optional())); +} + +TEST_F(RTCCertificateGeneratorTest, GenerateAsyncECDSA) { + EXPECT_FALSE(fixture_->certificate()); + fixture_->generator()->GenerateCertificateAsync( + KeyParams::ECDSA(), + Optional(), + fixture_); + // Until generation has completed, the certificate is null. Since this is an + // async call, generation must not have completed until we process messages + // posted to this thread (which is done by |EXPECT_TRUE_WAIT|). + EXPECT_FALSE(fixture_->GenerateAsyncCompleted()); + EXPECT_FALSE(fixture_->certificate()); + EXPECT_TRUE_WAIT(fixture_->GenerateAsyncCompleted(), kGenerationTimeoutMs); + EXPECT_TRUE(fixture_->certificate()); +} + +TEST_F(RTCCertificateGeneratorTest, GenerateWithExpires) { + // By generating two certificates with different expiration we can compare the + // two expiration times relative to each other without knowing the current + // time relative to epoch, 1970-01-01T00:00:00Z. This verifies that the + // expiration parameter is correctly used relative to the generator's clock, + // but does not verify that this clock is relative to epoch. + + // Generate a certificate that expires immediately. + scoped_refptr cert_a = + RTCCertificateGenerator::GenerateCertificate( + KeyParams::ECDSA(), Optional(0)); + EXPECT_TRUE(cert_a); + + // Generate a certificate that expires in one minute. + const uint64_t kExpiresMs = 60000; + scoped_refptr cert_b = + RTCCertificateGenerator::GenerateCertificate( + KeyParams::ECDSA(), Optional(kExpiresMs)); + EXPECT_TRUE(cert_b); + + // Verify that |cert_b| expires approximately |kExpiresMs| after |cert_a| + // (allowing a +/- 1 second plus maximum generation time difference). + EXPECT_GT(cert_b->Expires(), cert_a->Expires()); + uint64_t expires_diff = cert_b->Expires() - cert_a->Expires(); + EXPECT_GE(expires_diff, kExpiresMs); + EXPECT_LE(expires_diff, kExpiresMs + 2*kGenerationTimeoutMs + 1000); +} + +TEST_F(RTCCertificateGeneratorTest, GenerateWithInvalidParamsShouldFail) { + KeyParams invalid_params = KeyParams::RSA(0, 0); + EXPECT_FALSE(invalid_params.IsValid()); + + EXPECT_FALSE(RTCCertificateGenerator::GenerateCertificate( + invalid_params, Optional())); + + fixture_->generator()->GenerateCertificateAsync( + invalid_params, + Optional(), + fixture_); + EXPECT_TRUE_WAIT(fixture_->GenerateAsyncCompleted(), kGenerationTimeoutMs); + EXPECT_FALSE(fixture_->certificate()); +} + +} // namespace rtc