aboutsummaryrefslogtreecommitdiff
path: root/cast/sender
diff options
context:
space:
mode:
authorbtolsch <btolsch@chromium.org>2019-09-10 10:42:56 -0700
committerCommit Bot <commit-bot@chromium.org>2019-09-10 18:04:35 +0000
commit06a865967c70f8d8e473b0ed40be2fdda09bd887 (patch)
treebfd71a6e552d98dc4af5cef2918681cc41231be5 /cast/sender
parent349c2aaf03b3aab25ff6a55b78f6da1d4cdceb64 (diff)
downloadopenscreen-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/DEPS2
-rw-r--r--cast/sender/channel/BUILD.gn30
-rw-r--r--cast/sender/channel/cast_auth_util.cc379
-rw-r--r--cast/sender/channel/cast_auth_util.h95
-rw-r--r--cast/sender/channel/cast_auth_util_unittest.cc472
-rw-r--r--cast/sender/channel/proto/BUILD.gn11
-rw-r--r--cast/sender/channel/proto/cast_channel.proto99
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, &not_before, &not_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, &not_before, &not_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, &not_before, &not_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, &not_before, &not_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;
+}