diff options
author | btolsch <btolsch@chromium.org> | 2019-09-10 10:42:56 -0700 |
---|---|---|
committer | Commit Bot <commit-bot@chromium.org> | 2019-09-10 18:04:35 +0000 |
commit | 06a865967c70f8d8e473b0ed40be2fdda09bd887 (patch) | |
tree | bfd71a6e552d98dc4af5cef2918681cc41231be5 /cast/sender | |
parent | 349c2aaf03b3aab25ff6a55b78f6da1d4cdceb64 (diff) | |
download | openscreen-06a865967c70f8d8e473b0ed40be2fdda09bd887.tar.gz |
Add cast channel authentication utilities
This change imports cast_auth_util.cc from Chromium's sender component
(//components/cast_channel).
Bug: openscreen:59
Change-Id: I4eb3a66ae1e50dbe0435176a15d13b2c62d452f3
Reviewed-on: https://chromium-review.googlesource.com/c/openscreen/+/1772435
Commit-Queue: Brandon Tolsch <btolsch@chromium.org>
Reviewed-by: Ryan Keane <rwkeane@google.com>
Reviewed-by: Max Yakimakha <yakimakha@chromium.org>
Diffstat (limited to 'cast/sender')
-rw-r--r-- | cast/sender/DEPS | 2 | ||||
-rw-r--r-- | cast/sender/channel/BUILD.gn | 30 | ||||
-rw-r--r-- | cast/sender/channel/cast_auth_util.cc | 379 | ||||
-rw-r--r-- | cast/sender/channel/cast_auth_util.h | 95 | ||||
-rw-r--r-- | cast/sender/channel/cast_auth_util_unittest.cc | 472 | ||||
-rw-r--r-- | cast/sender/channel/proto/BUILD.gn | 11 | ||||
-rw-r--r-- | cast/sender/channel/proto/cast_channel.proto | 99 |
7 files changed, 1087 insertions, 1 deletions
diff --git a/cast/sender/DEPS b/cast/sender/DEPS index e386f11a..7ab7a51a 100644 --- a/cast/sender/DEPS +++ b/cast/sender/DEPS @@ -2,6 +2,6 @@ include_rules = [ # libcast sender code must not depend on the receiver. - '+cast/common/public', + '+cast/common', '+cast/sender' ] diff --git a/cast/sender/channel/BUILD.gn b/cast/sender/channel/BUILD.gn new file mode 100644 index 00000000..c383b6fa --- /dev/null +++ b/cast/sender/channel/BUILD.gn @@ -0,0 +1,30 @@ +# Copyright 2019 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. + +source_set("channel") { + sources = [ + "cast_auth_util.cc", + "cast_auth_util.h", + ] + + deps = [ + "../../../platform", + "proto", + ] +} + +source_set("unittests") { + testonly = true + sources = [ + "cast_auth_util_unittest.cc", + ] + + deps = [ + ":channel", + "../../../platform", + "../../../third_party/googletest:gtest", + "../../common/certificate/proto:unittest_proto", + "proto", + ] +} diff --git a/cast/sender/channel/cast_auth_util.cc b/cast/sender/channel/cast_auth_util.cc new file mode 100644 index 00000000..ec9a889b --- /dev/null +++ b/cast/sender/channel/cast_auth_util.cc @@ -0,0 +1,379 @@ +// Copyright 2019 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 "cast/sender/channel/cast_auth_util.h" + +#include <openssl/rand.h> + +#include <vector> + +#include "cast/common/certificate/cast_cert_validator.h" +#include "cast/common/certificate/cast_cert_validator_internal.h" +#include "cast/common/certificate/cast_crl.h" +#include "platform/api/logging.h" +#include "platform/api/time.h" +#include "platform/base/error.h" + +namespace cast { +namespace channel { +namespace { + +#define PARSE_ERROR_PREFIX "Failed to parse auth message: " + +// The maximum number of days a cert can live for. +const int kMaxSelfSignedCertLifetimeInDays = 4; + +// The size of the nonce challenge in bytes. +const int kNonceSizeInBytes = 16; + +// The number of hours after which a nonce is regenerated. +long kNonceExpirationTimeInHours = 24; + +using CastCertError = openscreen::Error::Code; + +// Extracts an embedded DeviceAuthMessage payload from an auth challenge reply +// message. +openscreen::Error ParseAuthMessage(const CastMessage& challenge_reply, + DeviceAuthMessage* auth_message) { + if (challenge_reply.payload_type() != CastMessage_PayloadType_BINARY) { + return openscreen::Error(CastCertError::kCastV2WrongPayloadType, + PARSE_ERROR_PREFIX + "Wrong payload type in challenge reply"); + } + if (!challenge_reply.has_payload_binary()) { + return openscreen::Error( + CastCertError::kCastV2NoPayload, PARSE_ERROR_PREFIX + "Payload type is binary but payload_binary field not set"); + } + if (!auth_message->ParseFromString(challenge_reply.payload_binary())) { + return openscreen::Error( + CastCertError::kCastV2PayloadParsingFailed, PARSE_ERROR_PREFIX + "Cannot parse binary payload into DeviceAuthMessage"); + } + + if (auth_message->has_error()) { + std::stringstream ss; + ss << PARSE_ERROR_PREFIX "Auth message error: " + << auth_message->error().error_type(); + return openscreen::Error(CastCertError::kCastV2MessageError, ss.str()); + } + if (!auth_message->has_response()) { + return openscreen::Error(CastCertError::kCastV2NoResponse, + PARSE_ERROR_PREFIX + "Auth message has no response field"); + } + return openscreen::Error::None(); +} + +class CastNonce { + public: + static CastNonce* GetInstance() { + static CastNonce* cast_nonce = new CastNonce(); + return cast_nonce; + } + + static const std::string& Get() { + GetInstance()->EnsureNonceTimely(); + return GetInstance()->nonce_; + } + + private: + CastNonce() : nonce_(kNonceSizeInBytes, 0) { GenerateNonce(); } + void GenerateNonce() { + OSP_CHECK_EQ( + RAND_bytes(reinterpret_cast<uint8_t*>(&nonce_[0]), kNonceSizeInBytes), + 1); + nonce_generation_time_ = openscreen::platform::GetWallTimeSinceUnixEpoch(); + } + + void EnsureNonceTimely() { + if (openscreen::platform::GetWallTimeSinceUnixEpoch() > + (nonce_generation_time_ + + std::chrono::hours(kNonceExpirationTimeInHours))) { + GenerateNonce(); + } + } + + // The nonce challenge to send to the Cast receiver. + // The nonce is updated daily. + std::string nonce_; + std::chrono::seconds nonce_generation_time_; +}; + +// Maps CastCertError from certificate verification to openscreen::Error. +// If crl_required is set to false, all revocation related errors are ignored. +openscreen::Error MapToOpenscreenError(CastCertError error, bool crl_required) { + switch (error) { + case CastCertError::kErrCertsMissing: + return openscreen::Error(CastCertError::kCastV2PeerCertEmpty, + "Failed to locate certificates."); + case CastCertError::kErrCertsParse: + return openscreen::Error(CastCertError::kErrCertsParse, + "Failed to parse certificates."); + case CastCertError::kErrCertsDateInvalid: + return openscreen::Error(CastCertError::kCastV2CertNotSignedByTrustedCa, + "Failed date validity check."); + case CastCertError::kErrCertsVerifyGeneric: + return openscreen::Error( + CastCertError::kCastV2CertNotSignedByTrustedCa, + "Failed with a generic certificate verification error."); + case CastCertError::kErrCertsRestrictions: + return openscreen::Error(CastCertError::kCastV2CertNotSignedByTrustedCa, + "Failed certificate restrictions."); + case CastCertError::kErrCrlInvalid: + // This error is only encountered if |crl_required| is true. + OSP_DCHECK(crl_required); + return openscreen::Error(CastCertError::kErrCrlInvalid, + "Failed to provide a valid CRL."); + case CastCertError::kErrCertsRevoked: + return openscreen::Error(CastCertError::kErrCertsRevoked, + "Failed certificate revocation check."); + case CastCertError::kNone: + return openscreen::Error::None(); + default: + return openscreen::Error(CastCertError::kCastV2CertNotSignedByTrustedCa, + "Failed verifying cast device certificate."); + } + return openscreen::Error::None(); +} + +openscreen::Error VerifyAndMapDigestAlgorithm( + HashAlgorithm response_digest_algorithm, + certificate::DigestAlgorithm* digest_algorithm, + bool enforce_sha256_checking) { + switch (response_digest_algorithm) { + case SHA1: + if (enforce_sha256_checking) { + return openscreen::Error(CastCertError::kCastV2DigestUnsupported, + "Unsupported digest algorithm."); + } + *digest_algorithm = certificate::DigestAlgorithm::kSha1; + break; + case SHA256: + *digest_algorithm = certificate::DigestAlgorithm::kSha256; + break; + default: + return CastCertError::kCastV2DigestUnsupported; + } + return openscreen::Error::None(); +} + +} // namespace + +// static +AuthContext AuthContext::Create() { + return AuthContext(CastNonce::Get()); +} + +AuthContext::AuthContext(const std::string& nonce) : nonce_(nonce) {} + +AuthContext::~AuthContext() {} + +openscreen::Error AuthContext::VerifySenderNonce( + const std::string& nonce_response, + bool enforce_nonce_checking) const { + if (nonce_ != nonce_response) { + if (enforce_nonce_checking) { + return openscreen::Error(CastCertError::kCastV2SenderNonceMismatch, + "Sender nonce mismatched."); + } + } + return openscreen::Error::None(); +} + +openscreen::Error VerifyTLSCertificateValidity( + X509* peer_cert, + std::chrono::seconds verification_time) { + // Ensure the peer cert is valid and doesn't have an excessive remaining + // lifetime. Although it is not verified as an X.509 certificate, the entire + // structure is signed by the AuthResponse, so the validity field from X.509 + // is repurposed as this signature's expiration. + certificate::DateTime not_before; + certificate::DateTime not_after; + if (!certificate::GetCertValidTimeRange(peer_cert, ¬_before, ¬_after)) { + return openscreen::Error(CastCertError::kErrCertsParse, PARSE_ERROR_PREFIX + "Parsing validity fields failed."); + } + + std::chrono::seconds lifetime_limit = + verification_time + + std::chrono::hours(24 * kMaxSelfSignedCertLifetimeInDays); + certificate::DateTime verification_time_exploded = {}; + certificate::DateTime lifetime_limit_exploded = {}; + OSP_CHECK(certificate::DateTimeFromSeconds(verification_time.count(), + &verification_time_exploded)); + OSP_CHECK(certificate::DateTimeFromSeconds(lifetime_limit.count(), + &lifetime_limit_exploded)); + if (verification_time_exploded < not_before) { + return openscreen::Error( + CastCertError::kCastV2TlsCertValidStartDateInFuture, + PARSE_ERROR_PREFIX "Certificate's valid start date is in the future."); + } + if (not_after < verification_time_exploded) { + return openscreen::Error(CastCertError::kCastV2TlsCertExpired, + PARSE_ERROR_PREFIX "Certificate has expired."); + } + if (lifetime_limit_exploded < not_after) { + return openscreen::Error(CastCertError::kCastV2TlsCertValidityPeriodTooLong, + PARSE_ERROR_PREFIX + "Peer cert lifetime is too long."); + } + return openscreen::Error::None(); +} + +ErrorOr<CastDeviceCertPolicy> AuthenticateChallengeReply( + const CastMessage& challenge_reply, + X509* peer_cert, + const AuthContext& auth_context) { + DeviceAuthMessage auth_message; + openscreen::Error result = ParseAuthMessage(challenge_reply, &auth_message); + if (!result.ok()) { + return result; + } + + result = VerifyTLSCertificateValidity( + peer_cert, openscreen::platform::GetWallTimeSinceUnixEpoch()); + if (!result.ok()) { + return result; + } + + const AuthResponse& response = auth_message.response(); + const std::string& nonce_response = response.sender_nonce(); + + result = auth_context.VerifySenderNonce(nonce_response); + if (!result.ok()) { + return result; + } + + int len = i2d_X509(peer_cert, nullptr); + if (len <= 0) { + return openscreen::Error(CastCertError::kErrCertsParse, + "Serializing cert failed."); + } + std::string peer_cert_der(len, 0); + uint8_t* data = reinterpret_cast<uint8_t*>(&peer_cert_der[0]); + if (!i2d_X509(peer_cert, &data)) { + return openscreen::Error(CastCertError::kErrCertsParse, + "Serializing cert failed."); + } + size_t actual_size = data - reinterpret_cast<uint8_t*>(&peer_cert_der[0]); + OSP_DCHECK_EQ(actual_size, peer_cert_der.size()); + peer_cert_der.resize(actual_size); + + return VerifyCredentials(response, nonce_response + peer_cert_der); +} + +// This function does the following +// +// * Verifies that the certificate chain |response.client_auth_certificate| + +// |response.intermediate_certificate| is valid and chains to a trusted Cast +// root. The list of trusted Cast roots can be overrided by providing a +// non-nullptr |cast_trust_store|. The certificate is verified at +// |verification_time|. +// +// * Verifies that none of the certificates in the chain are revoked based on +// the CRL provided in the response |response.crl|. The CRL is verified to be +// valid and its issuer certificate chains to a trusted Cast CRL root. The +// list of trusted Cast CRL roots can be overrided by providing a non-nullptr +// |crl_trust_store|. If |crl_policy| is kCrlOptional then the result of +// revocation checking is ignored. The CRL is verified at |verification_time|. +// +// * Verifies that |response.signature| matches the signature of +// |signature_input| by |response.client_auth_certificate|'s public key. +ErrorOr<CastDeviceCertPolicy> VerifyCredentialsImpl( + const AuthResponse& response, + const std::string& signature_input, + const certificate::CRLPolicy& crl_policy, + certificate::TrustStore* cast_trust_store, + certificate::TrustStore* crl_trust_store, + const certificate::DateTime& verification_time, + bool enforce_sha256_checking) { + if (response.signature().empty() && !signature_input.empty()) { + return openscreen::Error(CastCertError::kCastV2SignatureEmpty, + "Signature is empty."); + } + + // Verify the certificate + std::unique_ptr<certificate::CertVerificationContext> verification_context; + + // Build a single vector containing the certificate chain. + std::vector<std::string> cert_chain; + cert_chain.push_back(response.client_auth_certificate()); + cert_chain.insert(cert_chain.end(), + response.intermediate_certificate().begin(), + response.intermediate_certificate().end()); + + // Parse the CRL. + std::unique_ptr<certificate::CastCRL> crl; + if (!response.crl().empty()) { + crl = certificate::ParseAndVerifyCRL(response.crl(), verification_time, + crl_trust_store); + } + + // Perform certificate verification. + certificate::CastDeviceCertPolicy device_policy; + openscreen::Error verify_result = certificate::VerifyDeviceCert( + cert_chain, verification_time, &verification_context, &device_policy, + crl.get(), crl_policy, cast_trust_store); + + // Handle and report errors. + openscreen::Error result = MapToOpenscreenError( + verify_result.code(), crl_policy == certificate::CRLPolicy::kCrlRequired); + if (!result.ok()) { + return result; + } + + // The certificate is verified at this point. + certificate::DigestAlgorithm digest_algorithm; + openscreen::Error digest_result = VerifyAndMapDigestAlgorithm( + response.hash_algorithm(), &digest_algorithm, enforce_sha256_checking); + if (!digest_result.ok()) { + return digest_result; + } + + certificate::ConstDataSpan signature = { + reinterpret_cast<const uint8_t*>(response.signature().data()), + static_cast<uint32_t>(response.signature().size())}; + certificate::ConstDataSpan siginput = { + reinterpret_cast<const uint8_t*>(signature_input.data()), + static_cast<uint32_t>(signature_input.size())}; + if (!verification_context->VerifySignatureOverData(signature, siginput, + digest_algorithm)) { + return openscreen::Error(CastCertError::kCastV2SignedBlobsMismatch, + "Failed verifying signature over data."); + } + + return device_policy; +} + +ErrorOr<CastDeviceCertPolicy> VerifyCredentials( + const AuthResponse& response, + const std::string& signature_input, + bool enforce_revocation_checking, + bool enforce_sha256_checking) { + certificate::DateTime now = {}; + OSP_CHECK(certificate::DateTimeFromSeconds( + openscreen::platform::GetWallTimeSinceUnixEpoch().count(), &now)); + certificate::CRLPolicy policy = (enforce_revocation_checking) + ? certificate::CRLPolicy::kCrlRequired + : certificate::CRLPolicy::kCrlOptional; + return VerifyCredentialsImpl(response, signature_input, policy, nullptr, + nullptr, now, enforce_sha256_checking); +} + +ErrorOr<CastDeviceCertPolicy> VerifyCredentialsForTest( + const AuthResponse& response, + const std::string& signature_input, + certificate::CRLPolicy crl_policy, + certificate::TrustStore* cast_trust_store, + certificate::TrustStore* crl_trust_store, + const certificate::DateTime& verification_time, + bool enforce_sha256_checking) { + return VerifyCredentialsImpl(response, signature_input, crl_policy, + cast_trust_store, crl_trust_store, + verification_time, enforce_sha256_checking); +} + +} // namespace channel +} // namespace cast diff --git a/cast/sender/channel/cast_auth_util.h b/cast/sender/channel/cast_auth_util.h new file mode 100644 index 00000000..35a9a704 --- /dev/null +++ b/cast/sender/channel/cast_auth_util.h @@ -0,0 +1,95 @@ +// Copyright 2019 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. + +#ifndef CAST_SENDER_CHANNEL_CAST_AUTH_UTIL_H_ +#define CAST_SENDER_CHANNEL_CAST_AUTH_UTIL_H_ + +#include <openssl/x509.h> + +#include <string> + +#include "cast/common/certificate/cast_cert_validator.h" +#include "cast/sender/channel/proto/cast_channel.pb.h" +#include "platform/base/error.h" + +namespace cast { +namespace certificate { +enum class CRLPolicy; +struct DateTime; +struct TrustStore; +} // namespace certificate +} // namespace cast + +namespace cast { +namespace channel { + +class AuthResponse; +class CastMessage; + +template <typename T> +using ErrorOr = openscreen::ErrorOr<T>; +using CastDeviceCertPolicy = certificate::CastDeviceCertPolicy; + +class AuthContext { + public: + ~AuthContext(); + + // Get an auth challenge context. + // The same context must be used in the challenge and reply. + static AuthContext Create(); + + // Verifies the nonce received in the response is equivalent to the one sent. + // Returns success if |nonce_response| matches nonce_ + openscreen::Error VerifySenderNonce( + const std::string& nonce_response, + bool enforce_nonce_checking = false) const; + + // The nonce challenge. + const std::string& nonce() const { return nonce_; } + + private: + explicit AuthContext(const std::string& nonce); + + const std::string nonce_; +}; + +// Authenticates the given |challenge_reply|: +// 1. Signature contained in the reply is valid. +// 2. Certficate used to sign is rooted to a trusted CA. +ErrorOr<CastDeviceCertPolicy> AuthenticateChallengeReply( + const CastMessage& challenge_reply, + X509* peer_cert, + const AuthContext& auth_context); + +// Performs a quick check of the TLS certificate for time validity requirements. +openscreen::Error VerifyTLSCertificateValidity( + X509* peer_cert, + std::chrono::seconds verification_time); + +// Auth-library specific implementation of cryptographic signature verification +// routines. Verifies that |response| contains a valid signature of +// |signature_input|. +ErrorOr<CastDeviceCertPolicy> VerifyCredentials( + const AuthResponse& response, + const std::string& signature_input, + bool enforce_revocation_checking = false, + bool enforce_sha256_checking = false); + +// Exposed for testing only. +// +// Overloaded version of VerifyCredentials that allows modifying +// the crl policy, trust stores, and verification times. +ErrorOr<CastDeviceCertPolicy> VerifyCredentialsForTest( + const AuthResponse& response, + const std::string& signature_input, + certificate::CRLPolicy crl_policy, + certificate::TrustStore* cast_trust_store, + certificate::TrustStore* crl_trust_store, + const certificate::DateTime& verification_time, + bool enforce_sha256_checking = false); + +} // namespace channel +} // namespace cast + +#endif // CAST_SENDER_CHANNEL_CAST_AUTH_UTIL_H_ diff --git a/cast/sender/channel/cast_auth_util_unittest.cc b/cast/sender/channel/cast_auth_util_unittest.cc new file mode 100644 index 00000000..b8c94d53 --- /dev/null +++ b/cast/sender/channel/cast_auth_util_unittest.cc @@ -0,0 +1,472 @@ +// Copyright 2019 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 "cast/sender/channel/cast_auth_util.h" + +#include <string> + +#include "cast/common/certificate/cast_cert_validator.h" +#include "cast/common/certificate/cast_crl.h" +#include "cast/common/certificate/proto/test_suite.pb.h" +#include "cast/common/certificate/test_helpers.h" +#include "cast/sender/channel/proto/cast_channel.pb.h" +#include "gtest/gtest.h" +#include "platform/api/logging.h" +#include "platform/api/time.h" + +namespace cast { +namespace channel { +namespace { + +using ErrorCode = openscreen::Error::Code; + +bool ConvertTimeSeconds(const certificate::DateTime& time, uint64_t* seconds) { + static constexpr uint64_t kDaysPerYear = 365; + static constexpr uint64_t kHoursPerDay = 24; + static constexpr uint64_t kMinutesPerHour = 60; + static constexpr uint64_t kSecondsPerMinute = 60; + + static constexpr uint64_t kSecondsPerDay = + kSecondsPerMinute * kMinutesPerHour * kHoursPerDay; + static constexpr uint64_t kDaysPerQuadYear = 4 * kDaysPerYear + 1; + static constexpr uint64_t kDaysPerCentury = + kDaysPerQuadYear * 24 + kDaysPerYear * 4; + static constexpr uint64_t kDaysPerQuadCentury = 4 * kDaysPerCentury + 1; + + static constexpr uint64_t kDaysPerMonth[] = { + 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, + }; + + bool is_leap_year = + (time.year % 4 == 0 && (time.year % 100 != 0 || time.year % 400 == 0)); + if (time.year < 1970 || time.month < 1 || time.day < 1 || + time.day > (kDaysPerMonth[time.month - 1] + is_leap_year) || + time.month > 12 || time.hour > 23 || time.minute > 59 || + time.second > 60) { + return false; + } + uint64_t result = 0; + uint64_t year = time.year - 1970; + uint64_t first_two_years = year >= 2; + result += first_two_years * 2 * kDaysPerYear * kSecondsPerDay; + year -= first_two_years * 2; + + if (first_two_years) { + uint64_t twenty_eight_years = year >= 28; + result += twenty_eight_years * 7 * kDaysPerQuadYear * kSecondsPerDay; + year -= twenty_eight_years * 28; + + if (twenty_eight_years) { + uint64_t quad_centuries = year / 400; + result += quad_centuries * kDaysPerQuadCentury * kSecondsPerDay; + year -= quad_centuries * 400; + + uint64_t first_century = year >= 100; + result += first_century * (kDaysPerCentury + 1) * kSecondsPerDay; + year -= first_century * 100; + + uint64_t centuries = year / 100; + result += centuries * kDaysPerCentury * kSecondsPerDay; + year -= centuries * 100; + } + + uint64_t quad_years = year / 4; + result += quad_years * kDaysPerQuadYear * kSecondsPerDay; + year -= quad_years * 4; + + uint64_t first_year = year >= 1; + result += first_year * (kDaysPerYear + 1) * kSecondsPerDay; + year -= first_year; + + result += year * kDaysPerYear * kSecondsPerDay; + OSP_DCHECK_LE(year, 2); + } + + for (int i = 0; i < time.month - 1; ++i) { + uint64_t days = kDaysPerMonth[i]; + result += days * kSecondsPerDay; + } + if (time.month >= 3 && is_leap_year) { + result += kSecondsPerDay; + } + result += (time.day - 1) * kSecondsPerDay; + result += time.hour * kMinutesPerHour * kSecondsPerMinute; + result += time.minute * kSecondsPerMinute; + result += time.second; + + *seconds = result; + return true; +} + +#define TEST_DATA_PREFIX "test/data/cast/common/certificate/" + +class CastAuthUtilTest : public testing::Test { + public: + CastAuthUtilTest() {} + ~CastAuthUtilTest() override {} + + void SetUp() override {} + + protected: + static AuthResponse CreateAuthResponse(std::string* signed_data, + HashAlgorithm digest_algorithm) { + std::vector<std::string> chain = + certificate::testing::ReadCertificatesFromPemFile( + TEST_DATA_PREFIX "certificates/chromecast_gen1.pem"); + OSP_CHECK(!chain.empty()); + + certificate::testing::SignatureTestData signatures = + certificate::testing::ReadSignatureTestData( + TEST_DATA_PREFIX "signeddata/2ZZBG9_FA8FCA3EF91A.pem"); + + AuthResponse response; + + response.set_client_auth_certificate(chain[0]); + for (size_t i = 1; i < chain.size(); ++i) + response.add_intermediate_certificate(chain[i]); + + response.set_hash_algorithm(digest_algorithm); + switch (digest_algorithm) { + case SHA1: + response.set_signature( + std::string(reinterpret_cast<const char*>(signatures.sha1.data), + signatures.sha1.length)); + break; + case SHA256: + response.set_signature( + std::string(reinterpret_cast<const char*>(signatures.sha256.data), + signatures.sha256.length)); + break; + } + signed_data->assign(reinterpret_cast<const char*>(signatures.message.data), + signatures.message.length); + + return response; + } + + // Mangles a string by inverting the first byte. + static void MangleString(std::string* str) { (*str)[0] = ~(*str)[0]; } +}; + +// Note on expiration: VerifyCredentials() depends on the system clock. In +// practice this shouldn't be a problem though since the certificate chain +// being verified doesn't expire until 2032. +TEST_F(CastAuthUtilTest, VerifySuccess) { + std::string signed_data; + AuthResponse auth_response = CreateAuthResponse(&signed_data, SHA256); + certificate::DateTime now = {}; + ASSERT_TRUE(certificate::DateTimeFromSeconds( + openscreen::platform::GetWallTimeSinceUnixEpoch().count(), &now)); + ErrorOr<CastDeviceCertPolicy> result = VerifyCredentialsForTest( + auth_response, signed_data, certificate::CRLPolicy::kCrlOptional, nullptr, + nullptr, now); + EXPECT_TRUE(result); + EXPECT_EQ(certificate::CastDeviceCertPolicy::kUnrestricted, result.value()); +} + +TEST_F(CastAuthUtilTest, VerifyBadCA) { + std::string signed_data; + AuthResponse auth_response = CreateAuthResponse(&signed_data, SHA256); + MangleString(auth_response.mutable_intermediate_certificate(0)); + ErrorOr<CastDeviceCertPolicy> result = + VerifyCredentials(auth_response, signed_data); + EXPECT_FALSE(result); + EXPECT_EQ(ErrorCode::kErrCertsParse, result.error().code()); +} + +TEST_F(CastAuthUtilTest, VerifyBadClientAuthCert) { + std::string signed_data; + AuthResponse auth_response = CreateAuthResponse(&signed_data, SHA256); + MangleString(auth_response.mutable_client_auth_certificate()); + ErrorOr<CastDeviceCertPolicy> result = + VerifyCredentials(auth_response, signed_data); + EXPECT_FALSE(result); + EXPECT_EQ(ErrorCode::kErrCertsParse, result.error().code()); +} + +TEST_F(CastAuthUtilTest, VerifyBadSignature) { + std::string signed_data; + AuthResponse auth_response = CreateAuthResponse(&signed_data, SHA256); + MangleString(auth_response.mutable_signature()); + ErrorOr<CastDeviceCertPolicy> result = + VerifyCredentials(auth_response, signed_data); + EXPECT_FALSE(result); + EXPECT_EQ(ErrorCode::kCastV2SignedBlobsMismatch, result.error().code()); +} + +TEST_F(CastAuthUtilTest, VerifyEmptySignature) { + std::string signed_data; + AuthResponse auth_response = CreateAuthResponse(&signed_data, SHA256); + auth_response.mutable_signature()->clear(); + ErrorOr<CastDeviceCertPolicy> result = + VerifyCredentials(auth_response, signed_data); + EXPECT_FALSE(result); + EXPECT_EQ(ErrorCode::kCastV2SignatureEmpty, result.error().code()); +} + +TEST_F(CastAuthUtilTest, VerifyUnsupportedDigest) { + std::string signed_data; + AuthResponse auth_response = CreateAuthResponse(&signed_data, SHA1); + certificate::DateTime now = {}; + ASSERT_TRUE(certificate::DateTimeFromSeconds( + openscreen::platform::GetWallTimeSinceUnixEpoch().count(), &now)); + ErrorOr<CastDeviceCertPolicy> result = VerifyCredentialsForTest( + auth_response, signed_data, certificate::CRLPolicy::kCrlOptional, nullptr, + nullptr, now, true); + EXPECT_FALSE(result); + EXPECT_EQ(ErrorCode::kCastV2DigestUnsupported, result.error().code()); +} + +TEST_F(CastAuthUtilTest, VerifyBackwardsCompatibleDigest) { + std::string signed_data; + AuthResponse auth_response = CreateAuthResponse(&signed_data, SHA1); + certificate::DateTime now = {}; + ASSERT_TRUE(certificate::DateTimeFromSeconds( + openscreen::platform::GetWallTimeSinceUnixEpoch().count(), &now)); + ErrorOr<CastDeviceCertPolicy> result = VerifyCredentialsForTest( + auth_response, signed_data, certificate::CRLPolicy::kCrlOptional, nullptr, + nullptr, now); + EXPECT_TRUE(result); +} + +TEST_F(CastAuthUtilTest, VerifyBadPeerCert) { + std::string signed_data; + AuthResponse auth_response = CreateAuthResponse(&signed_data, SHA256); + MangleString(&signed_data); + ErrorOr<CastDeviceCertPolicy> result = + VerifyCredentials(auth_response, signed_data); + EXPECT_FALSE(result); + EXPECT_EQ(ErrorCode::kCastV2SignedBlobsMismatch, result.error().code()); +} + +TEST_F(CastAuthUtilTest, VerifySenderNonceMatch) { + AuthContext context = AuthContext::Create(); + ErrorOr<CastDeviceCertPolicy> result = + context.VerifySenderNonce(context.nonce(), true); + EXPECT_TRUE(result); +} + +TEST_F(CastAuthUtilTest, VerifySenderNonceMismatch) { + AuthContext context = AuthContext::Create(); + std::string received_nonce = "test2"; + EXPECT_NE(received_nonce, context.nonce()); + ErrorOr<CastDeviceCertPolicy> result = + context.VerifySenderNonce(received_nonce, true); + EXPECT_FALSE(result); + EXPECT_EQ(ErrorCode::kCastV2SenderNonceMismatch, result.error().code()); +} + +TEST_F(CastAuthUtilTest, VerifySenderNonceMissing) { + AuthContext context = AuthContext::Create(); + std::string received_nonce; + EXPECT_FALSE(context.nonce().empty()); + ErrorOr<CastDeviceCertPolicy> result = + context.VerifySenderNonce(received_nonce, true); + EXPECT_FALSE(result); + EXPECT_EQ(ErrorCode::kCastV2SenderNonceMismatch, result.error().code()); +} + +TEST_F(CastAuthUtilTest, VerifyTLSCertificateSuccess) { + std::vector<std::string> tls_cert_der = + certificate::testing::ReadCertificatesFromPemFile( + TEST_DATA_PREFIX "certificates/test_tls_cert.pem"); + std::string& der_cert = tls_cert_der[0]; + const uint8_t* data = (const uint8_t*)der_cert.data(); + X509* tls_cert = d2i_X509(nullptr, &data, der_cert.size()); + certificate::DateTime not_before; + certificate::DateTime not_after; + ASSERT_TRUE( + certificate::GetCertValidTimeRange(tls_cert, ¬_before, ¬_after)); + uint64_t x; + ASSERT_TRUE(ConvertTimeSeconds(not_before, &x)); + std::chrono::seconds s(x); + + ErrorOr<CastDeviceCertPolicy> result = + VerifyTLSCertificateValidity(tls_cert, s); + EXPECT_TRUE(result); + X509_free(tls_cert); +} + +TEST_F(CastAuthUtilTest, VerifyTLSCertificateTooEarly) { + std::vector<std::string> tls_cert_der = + certificate::testing::ReadCertificatesFromPemFile( + TEST_DATA_PREFIX "certificates/test_tls_cert.pem"); + std::string& der_cert = tls_cert_der[0]; + const uint8_t* data = (const uint8_t*)der_cert.data(); + X509* tls_cert = d2i_X509(nullptr, &data, der_cert.size()); + certificate::DateTime not_before; + certificate::DateTime not_after; + ASSERT_TRUE( + certificate::GetCertValidTimeRange(tls_cert, ¬_before, ¬_after)); + uint64_t x; + ASSERT_TRUE(ConvertTimeSeconds(not_before, &x)); + std::chrono::seconds s(x - 1); + + ErrorOr<CastDeviceCertPolicy> result = + VerifyTLSCertificateValidity(tls_cert, s); + EXPECT_FALSE(result); + EXPECT_EQ(ErrorCode::kCastV2TlsCertValidStartDateInFuture, + result.error().code()); + X509_free(tls_cert); +} + +TEST_F(CastAuthUtilTest, VerifyTLSCertificateTooLate) { + std::vector<std::string> tls_cert_der = + certificate::testing::ReadCertificatesFromPemFile( + TEST_DATA_PREFIX "certificates/test_tls_cert.pem"); + std::string& der_cert = tls_cert_der[0]; + const uint8_t* data = (const uint8_t*)der_cert.data(); + X509* tls_cert = d2i_X509(nullptr, &data, der_cert.size()); + certificate::DateTime not_before; + certificate::DateTime not_after; + ASSERT_TRUE( + certificate::GetCertValidTimeRange(tls_cert, ¬_before, ¬_after)); + uint64_t x; + ASSERT_TRUE(ConvertTimeSeconds(not_after, &x)); + std::chrono::seconds s(x + 2); + + ErrorOr<CastDeviceCertPolicy> result = + VerifyTLSCertificateValidity(tls_cert, s); + EXPECT_FALSE(result); + EXPECT_EQ(ErrorCode::kCastV2TlsCertExpired, result.error().code()); + X509_free(tls_cert); +} + +// Indicates the expected result of test step's verification. +enum TestStepResult { + RESULT_SUCCESS, + RESULT_FAIL, +}; + +// Verifies that the certificate chain provided is not revoked according to +// the provided Cast CRL at |verification_time|. +// The provided CRL is verified at |verification_time|. +// If |crl_required| is set, then a valid Cast CRL must be provided. +// Otherwise, a missing CRL is be ignored. +ErrorOr<CastDeviceCertPolicy> TestVerifyRevocation( + const std::vector<std::string>& certificate_chain, + const std::string& crl_bundle, + const certificate::DateTime& verification_time, + bool crl_required, + certificate::TrustStore* cast_trust_store, + certificate::TrustStore* crl_trust_store) { + AuthResponse response; + + if (certificate_chain.size() > 0) { + response.set_client_auth_certificate(certificate_chain[0]); + for (size_t i = 1; i < certificate_chain.size(); ++i) + response.add_intermediate_certificate(certificate_chain[i]); + } + + response.set_crl(crl_bundle); + + certificate::CRLPolicy crl_policy = certificate::CRLPolicy::kCrlRequired; + if (!crl_required && crl_bundle.empty()) + crl_policy = certificate::CRLPolicy::kCrlOptional; + ErrorOr<CastDeviceCertPolicy> result = + VerifyCredentialsForTest(response, "", crl_policy, cast_trust_store, + crl_trust_store, verification_time); + // This test doesn't set the signature so it will just fail there. + EXPECT_FALSE(result); + return result; +} + +// Runs a single test case. +bool RunTest(const certificate::DeviceCertTest& test_case) { + std::unique_ptr<certificate::TrustStore> crl_trust_store; + std::unique_ptr<certificate::TrustStore> cast_trust_store; + if (test_case.use_test_trust_anchors()) { + crl_trust_store = certificate::testing::CreateTrustStoreFromPemFile( + TEST_DATA_PREFIX "certificates/cast_crl_test_root_ca.pem"); + cast_trust_store = certificate::testing::CreateTrustStoreFromPemFile( + TEST_DATA_PREFIX "certificates/cast_test_root_ca.pem"); + + EXPECT_FALSE(crl_trust_store->certs.empty()); + EXPECT_FALSE(cast_trust_store->certs.empty()); + } + + std::vector<std::string> certificate_chain; + for (auto const& cert : test_case.der_cert_path()) { + certificate_chain.push_back(cert); + } + + // CastAuthUtil verifies the CRL at the same time as the certificate. + certificate::DateTime verification_time; + uint64_t cert_verify_time = test_case.cert_verification_time_seconds(); + if (!cert_verify_time) { + cert_verify_time = test_case.crl_verification_time_seconds(); + } + OSP_DCHECK( + certificate::DateTimeFromSeconds(cert_verify_time, &verification_time)); + + std::string crl_bundle = test_case.crl_bundle(); + ErrorOr<CastDeviceCertPolicy> result( + certificate::CastDeviceCertPolicy::kUnrestricted); + switch (test_case.expected_result()) { + case certificate::PATH_VERIFICATION_FAILED: + result = TestVerifyRevocation( + certificate_chain, crl_bundle, verification_time, false, + cast_trust_store.get(), crl_trust_store.get()); + EXPECT_EQ(result.error().code(), + ErrorCode::kCastV2CertNotSignedByTrustedCa); + return result.error().code() == + ErrorCode::kCastV2CertNotSignedByTrustedCa; + case certificate::CRL_VERIFICATION_FAILED: + // Fall-through intended. + case certificate::REVOCATION_CHECK_FAILED_WITHOUT_CRL: + result = TestVerifyRevocation( + certificate_chain, crl_bundle, verification_time, true, + cast_trust_store.get(), crl_trust_store.get()); + EXPECT_EQ(result.error().code(), ErrorCode::kErrCrlInvalid); + return result.error().code() == ErrorCode::kErrCrlInvalid; + case certificate::CRL_EXPIRED_AFTER_INITIAL_VERIFICATION: + // By-pass this test because CRL is always verified at the time the + // certificate is verified. + return true; + case certificate::REVOCATION_CHECK_FAILED: + result = TestVerifyRevocation( + certificate_chain, crl_bundle, verification_time, true, + cast_trust_store.get(), crl_trust_store.get()); + EXPECT_EQ(result.error().code(), ErrorCode::kErrCertsRevoked); + return result.error().code() == ErrorCode::kErrCertsRevoked; + case certificate::SUCCESS: + result = TestVerifyRevocation( + certificate_chain, crl_bundle, verification_time, false, + cast_trust_store.get(), crl_trust_store.get()); + EXPECT_EQ(result.error().code(), ErrorCode::kCastV2SignedBlobsMismatch); + return result.error().code() == ErrorCode::kCastV2SignedBlobsMismatch; + case certificate::UNSPECIFIED: + return false; + } + return false; +} + +// Parses the provided test suite provided in wire-format proto. +// Each test contains the inputs and the expected output. +// To see the description of the test, execute the test. +// These tests are generated by a test generator in google3. +void RunTestSuite(const std::string& test_suite_file_name) { + std::string testsuite_raw = + certificate::testing::ReadEntireFileToString(test_suite_file_name); + certificate::DeviceCertTestSuite test_suite; + EXPECT_TRUE(test_suite.ParseFromString(testsuite_raw)); + uint16_t successes = 0; + + for (auto const& test_case : test_suite.tests()) { + bool result = RunTest(test_case); + EXPECT_TRUE(result) << test_case.description(); + successes += result; + } + OSP_LOG_IF(ERROR, successes != test_suite.tests().size()) + << "successes: " << successes + << ", failures: " << (test_suite.tests().size() - successes); +} + +TEST_F(CastAuthUtilTest, CRLTestSuite) { + RunTestSuite("testsuite/testsuite1.pb"); +} + +} // namespace +} // namespace channel +} // namespace cast diff --git a/cast/sender/channel/proto/BUILD.gn b/cast/sender/channel/proto/BUILD.gn new file mode 100644 index 00000000..c3bfa439 --- /dev/null +++ b/cast/sender/channel/proto/BUILD.gn @@ -0,0 +1,11 @@ +# Copyright 2019 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. + +import("//third_party/protobuf/proto_library.gni") + +proto_library("proto") { + sources = [ + "cast_channel.proto", + ] +} diff --git a/cast/sender/channel/proto/cast_channel.proto b/cast/sender/channel/proto/cast_channel.proto new file mode 100644 index 00000000..57c7b3f3 --- /dev/null +++ b/cast/sender/channel/proto/cast_channel.proto @@ -0,0 +1,99 @@ +// Copyright 2019 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. + +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; + +package cast.channel; + +message CastMessage { + // Always pass a version of the protocol for future compatibility + // requirements. + enum ProtocolVersion { CASTV2_1_0 = 0; } + required ProtocolVersion protocol_version = 1; + + // source and destination ids identify the origin and destination of the + // message. They are used to route messages between endpoints that share a + // device-to-device channel. + // + // For messages between applications: + // - The sender application id is a unique identifier generated on behalf of + // the sender application. + // - The receiver id is always the the session id for the application. + // + // For messages to or from the sender or receiver platform, the special ids + // 'sender-0' and 'receiver-0' can be used. + // + // For messages intended for all endpoints using a given channel, the + // wildcard destination_id '*' can be used. + required string source_id = 2; + required string destination_id = 3; + + // This is the core multiplexing key. All messages are sent on a namespace + // and endpoints sharing a channel listen on one or more namespaces. The + // namespace defines the protocol and semantics of the message. + required string namespace = 4; + + // Encoding and payload info follows. + + // What type of data do we have in this message. + enum PayloadType { + STRING = 0; + BINARY = 1; + } + required PayloadType payload_type = 5; + + // Depending on payload_type, exactly one of the following optional fields + // will always be set. + optional string payload_utf8 = 6; + optional bytes payload_binary = 7; +} + +enum SignatureAlgorithm { + UNSPECIFIED = 0; + RSASSA_PKCS1v15 = 1; + RSASSA_PSS = 2; +} + +enum HashAlgorithm { + SHA1 = 0; + SHA256 = 1; +} + +// Messages for authentication protocol between a sender and a receiver. +message AuthChallenge { + optional SignatureAlgorithm signature_algorithm = 1 + [default = RSASSA_PKCS1v15]; + optional bytes sender_nonce = 2; + optional HashAlgorithm hash_algorithm = 3 [default = SHA1]; +} + +message AuthResponse { + required bytes signature = 1; + required bytes client_auth_certificate = 2; + repeated bytes intermediate_certificate = 3; + optional SignatureAlgorithm signature_algorithm = 4 + [default = RSASSA_PKCS1v15]; + optional bytes sender_nonce = 5; + optional HashAlgorithm hash_algorithm = 6 [default = SHA1]; + optional bytes crl = 7; +} + +message AuthError { + enum ErrorType { + INTERNAL_ERROR = 0; + NO_TLS = 1; // The underlying connection is not TLS + SIGNATURE_ALGORITHM_UNAVAILABLE = 2; + } + required ErrorType error_type = 1; +} + +message DeviceAuthMessage { + // Request fields + optional AuthChallenge challenge = 1; + // Response fields + optional AuthResponse response = 2; + optional AuthError error = 3; +} |