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 | |
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')
-rw-r--r-- | cast/common/certificate/BUILD.gn | 2 | ||||
-rw-r--r-- | cast/common/certificate/cast_cert_validator.cc | 3 | ||||
-rw-r--r-- | cast/common/certificate/cast_cert_validator_internal.cc | 83 | ||||
-rw-r--r-- | cast/common/certificate/cast_cert_validator_internal.h | 6 | ||||
-rw-r--r-- | cast/common/certificate/cast_cert_validator_unittest.cc | 49 | ||||
-rw-r--r-- | cast/common/certificate/cast_crl.cc | 8 | ||||
-rw-r--r-- | cast/common/certificate/cast_crl_unittest.cc | 101 | ||||
-rw-r--r-- | cast/common/certificate/test_helpers.cc | 85 | ||||
-rw-r--r-- | cast/common/certificate/test_helpers.h | 19 | ||||
-rw-r--r-- | cast/common/certificate/types.cc | 70 | ||||
-rw-r--r-- | cast/common/certificate/types.h | 4 | ||||
-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 |
18 files changed, 1333 insertions, 185 deletions
diff --git a/cast/common/certificate/BUILD.gn b/cast/common/certificate/BUILD.gn index 46449794..65000c5d 100644 --- a/cast/common/certificate/BUILD.gn +++ b/cast/common/certificate/BUILD.gn @@ -10,6 +10,8 @@ source_set("certificate") { "cast_cert_validator_internal.h", "cast_crl.cc", "cast_crl.h", + "types.cc", + "types.h", ] public_deps = [ "../../../third_party/boringssl", diff --git a/cast/common/certificate/cast_cert_validator.cc b/cast/common/certificate/cast_cert_validator.cc index fb7217ef..6fe95821 100644 --- a/cast/common/certificate/cast_cert_validator.cc +++ b/cast/common/certificate/cast_cert_validator.cc @@ -188,7 +188,8 @@ openscreen::Error VerifyDeviceCert( return error; } - if (crl && !crl->CheckRevocation(result_path.path, time)) { + if (crl_policy == CRLPolicy::kCrlRequired && + !crl->CheckRevocation(result_path.path, time)) { return CastCertError::kErrCertsRevoked; } diff --git a/cast/common/certificate/cast_cert_validator_internal.cc b/cast/common/certificate/cast_cert_validator_internal.cc index c704cf4c..39aff8a4 100644 --- a/cast/common/certificate/cast_cert_validator_internal.cc +++ b/cast/common/certificate/cast_cert_validator_internal.cc @@ -64,20 +64,9 @@ uint8_t ParseAsn1TimeDoubleDigit(ASN1_GENERALIZEDTIME* time, int index) { } CastCertError VerifyCertTime(X509* cert, const DateTime& time) { - ASN1_GENERALIZEDTIME* not_before_asn1 = ASN1_TIME_to_generalizedtime( - cert->cert_info->validity->notBefore, nullptr); - ASN1_GENERALIZEDTIME* not_after_asn1 = ASN1_TIME_to_generalizedtime( - cert->cert_info->validity->notAfter, nullptr); - if (!not_before_asn1 || !not_after_asn1) { - return CastCertError::kErrCertsVerifyGeneric; - } DateTime not_before; DateTime not_after; - bool times_valid = ParseAsn1GeneralizedTime(not_before_asn1, ¬_before) && - ParseAsn1GeneralizedTime(not_after_asn1, ¬_after); - ASN1_GENERALIZEDTIME_free(not_before_asn1); - ASN1_GENERALIZEDTIME_free(not_after_asn1); - if (!times_valid) { + if (!GetCertValidTimeRange(cert, ¬_before, ¬_after)) { return CastCertError::kErrCertsVerifyGeneric; } if ((time < not_before) || (not_after < time)) { @@ -350,6 +339,23 @@ bool ParseAsn1GeneralizedTime(ASN1_GENERALIZEDTIME* time, DateTime* out) { return true; } +bool GetCertValidTimeRange(X509* cert, + DateTime* not_before, + DateTime* not_after) { + ASN1_GENERALIZEDTIME* not_before_asn1 = ASN1_TIME_to_generalizedtime( + cert->cert_info->validity->notBefore, nullptr); + ASN1_GENERALIZEDTIME* not_after_asn1 = ASN1_TIME_to_generalizedtime( + cert->cert_info->validity->notAfter, nullptr); + if (!not_before_asn1 || !not_after_asn1) { + return false; + } + bool times_valid = ParseAsn1GeneralizedTime(not_before_asn1, not_before) && + ParseAsn1GeneralizedTime(not_after_asn1, not_after); + ASN1_GENERALIZEDTIME_free(not_before_asn1); + ASN1_GENERALIZEDTIME_free(not_after_asn1); + return times_valid; +} + bool VerifySignedData(const EVP_MD* digest, EVP_PKEY* public_key, const ConstDataSpan& data, @@ -364,59 +370,6 @@ bool VerifySignedData(const EVP_MD* digest, data.data, data.length) == 1); } -bool operator<(const DateTime& a, const DateTime& b) { - if (a.year < b.year) { - return true; - } else if (a.year > b.year) { - return false; - } - if (a.month < b.month) { - return true; - } else if (a.month > b.month) { - return false; - } - if (a.day < b.day) { - return true; - } else if (a.day > b.day) { - return false; - } - if (a.hour < b.hour) { - return true; - } else if (a.hour > b.hour) { - return false; - } - if (a.minute < b.minute) { - return true; - } else if (a.minute > b.minute) { - return false; - } - if (a.second < b.second) { - return true; - } else if (a.second > b.second) { - return false; - } - return false; -} - -bool ConvertTimeSeconds(uint64_t seconds, DateTime* time) { - struct tm tm = {}; - time_t sec = static_cast<time_t>(seconds); - OSP_DCHECK_GE(sec, 0); - OSP_DCHECK_EQ(static_cast<uint64_t>(sec), seconds); - if (!gmtime_r(&sec, &tm)) { - return false; - } - - time->second = tm.tm_sec; - time->minute = tm.tm_min; - time->hour = tm.tm_hour; - time->day = tm.tm_mday; - time->month = tm.tm_mon + 1; - time->year = tm.tm_year + 1900; - - return true; -} - openscreen::Error FindCertificatePath(const std::vector<std::string>& der_certs, const DateTime& time, CertificatePathResult* result_path, diff --git a/cast/common/certificate/cast_cert_validator_internal.h b/cast/common/certificate/cast_cert_validator_internal.h index ad8dafbc..1c127e72 100644 --- a/cast/common/certificate/cast_cert_validator_internal.h +++ b/cast/common/certificate/cast_cert_validator_internal.h @@ -34,12 +34,12 @@ bool VerifySignedData(const EVP_MD* digest, const ConstDataSpan& data, const ConstDataSpan& signature); -bool operator<(const DateTime& a, const DateTime& b); -bool ConvertTimeSeconds(uint64_t seconds, DateTime* time); - // Parses DateTime with additional restrictions laid out by RFC 5280 // 4.1.2.5.2. bool ParseAsn1GeneralizedTime(ASN1_GENERALIZEDTIME* time, DateTime* out); +bool GetCertValidTimeRange(X509* cert, + DateTime* not_before, + DateTime* not_after); struct CertificatePathResult { bssl::UniquePtr<X509> target_cert; diff --git a/cast/common/certificate/cast_cert_validator_unittest.cc b/cast/common/certificate/cast_cert_validator_unittest.cc index a4aa1962..7b0ee1d7 100644 --- a/cast/common/certificate/cast_cert_validator_unittest.cc +++ b/cast/common/certificate/cast_cert_validator_unittest.cc @@ -110,53 +110,16 @@ void RunTest(CastCertError expected_result, // If valid signatures are known for this device certificate, test them. if (!optional_signed_data_file_name.empty()) { - FILE* fp = fopen(optional_signed_data_file_name.c_str(), "r"); - ASSERT_TRUE(fp); - ConstDataSpan message = {}; - ConstDataSpan sha1 = {}; - ConstDataSpan sha256 = {}; - for (;;) { - char* name; - char* header; - unsigned char* data; - long length; - if (PEM_read(fp, &name, &header, &data, &length) == 1) { - if (strcmp(name, "MESSAGE") == 0) { - ASSERT_FALSE(message.data); - message.data = data; - message.length = length; - } else if (strcmp(name, "SIGNATURE SHA1") == 0) { - ASSERT_FALSE(sha1.data); - sha1.data = data; - sha1.length = length; - } else if (strcmp(name, "SIGNATURE SHA256") == 0) { - ASSERT_FALSE(sha256.data); - sha256.data = data; - sha256.length = length; - } else { - OPENSSL_free(data); - } - OPENSSL_free(name); - OPENSSL_free(header); - } else { - break; - } - } - ASSERT_TRUE(message.data); - ASSERT_TRUE(sha1.data); - ASSERT_TRUE(sha256.data); + testing::SignatureTestData signatures = + testing::ReadSignatureTestData(optional_signed_data_file_name); // Test verification of a valid SHA1 signature. - EXPECT_TRUE(context->VerifySignatureOverData(sha1, message, - DigestAlgorithm::kSha1)); + EXPECT_TRUE(context->VerifySignatureOverData( + signatures.sha1, signatures.message, DigestAlgorithm::kSha1)); // Test verification of a valid SHA256 signature. - EXPECT_TRUE(context->VerifySignatureOverData(sha256, message, - DigestAlgorithm::kSha256)); - - OPENSSL_free((uint8_t*)message.data); - OPENSSL_free((uint8_t*)sha1.data); - OPENSSL_free((uint8_t*)sha256.data); + EXPECT_TRUE(context->VerifySignatureOverData( + signatures.sha256, signatures.message, DigestAlgorithm::kSha256)); } } diff --git a/cast/common/certificate/cast_crl.cc b/cast/common/certificate/cast_crl.cc index a2f05ea0..901f5e0e 100644 --- a/cast/common/certificate/cast_crl.cc +++ b/cast/common/certificate/cast_crl.cc @@ -92,11 +92,11 @@ bool VerifyCRL(const Crl& crl, // Verify the CRL is still valid. DateTime not_before; - if (!ConvertTimeSeconds(tbs_crl.not_before_seconds(), ¬_before)) { + if (!DateTimeFromSeconds(tbs_crl.not_before_seconds(), ¬_before)) { return false; } DateTime not_after; - if (!ConvertTimeSeconds(tbs_crl.not_after_seconds(), ¬_after)) { + if (!DateTimeFromSeconds(tbs_crl.not_after_seconds(), ¬_after)) { return false; } if ((time < not_before) || (not_after < time)) { @@ -167,8 +167,8 @@ CastCRL::CastCRL(const TbsCrl& tbs_crl, const DateTime& overall_not_after) { // Parse the validity information. // Assume ConvertTimeSeconds will succeed. Successful call to VerifyCRL // means that these calls were successful. - ConvertTimeSeconds(tbs_crl.not_before_seconds(), ¬_before_); - ConvertTimeSeconds(tbs_crl.not_after_seconds(), ¬_after_); + DateTimeFromSeconds(tbs_crl.not_before_seconds(), ¬_before_); + DateTimeFromSeconds(tbs_crl.not_after_seconds(), ¬_after_); if (overall_not_after < not_after_) { not_after_ = overall_not_after; } diff --git a/cast/common/certificate/cast_crl_unittest.cc b/cast/common/certificate/cast_crl_unittest.cc index 2b17ea73..de3d922e 100644 --- a/cast/common/certificate/cast_crl_unittest.cc +++ b/cast/common/certificate/cast_crl_unittest.cc @@ -4,9 +4,6 @@ #include "cast/common/certificate/cast_crl.h" -#include <fcntl.h> -#include <unistd.h> - #include "cast/common/certificate/cast_cert_validator.h" #include "cast/common/certificate/cast_cert_validator_internal.h" #include "cast/common/certificate/proto/test_suite.pb.h" @@ -88,35 +85,19 @@ bool TestVerifyRevocation(CastCertError expected_result, return expected_result == result.code(); } -TrustStore CreateTrustStoreFromPemFile(const std::string& filename) { - TrustStore store; - - std::vector<std::string> certs = - testing::ReadCertificatesFromPemFile(filename); - for (const auto& der_cert : certs) { - const uint8_t* data = (const uint8_t*)der_cert.data(); - store.certs.emplace_back(d2i_X509(nullptr, &data, der_cert.size())); - } - return store; -} - #define TEST_DATA_PREFIX "test/data/cast/common/certificate/" bool RunTest(const DeviceCertTest& test_case) { - TrustStore crl_trust_store_local; - TrustStore cast_trust_store_local; - TrustStore* crl_trust_store = nullptr; - TrustStore* cast_trust_store = nullptr; + std::unique_ptr<TrustStore> crl_trust_store; + std::unique_ptr<TrustStore> cast_trust_store; if (test_case.use_test_trust_anchors()) { - crl_trust_store_local = CreateTrustStoreFromPemFile( + crl_trust_store = testing::CreateTrustStoreFromPemFile( TEST_DATA_PREFIX "certificates/cast_crl_test_root_ca.pem"); - cast_trust_store_local = CreateTrustStoreFromPemFile( + cast_trust_store = testing::CreateTrustStoreFromPemFile( TEST_DATA_PREFIX "certificates/cast_test_root_ca.pem"); - crl_trust_store = &crl_trust_store_local; - cast_trust_store = &cast_trust_store_local; - EXPECT_FALSE(crl_trust_store_local.certs.empty()); - EXPECT_FALSE(cast_trust_store_local.certs.empty()); + EXPECT_FALSE(crl_trust_store->certs.empty()); + EXPECT_FALSE(cast_trust_store->certs.empty()); } std::vector<std::string> der_cert_path; @@ -125,12 +106,12 @@ bool RunTest(const DeviceCertTest& test_case) { } DateTime cert_verification_time; - EXPECT_TRUE(ConvertTimeSeconds(test_case.cert_verification_time_seconds(), - &cert_verification_time)); + EXPECT_TRUE(DateTimeFromSeconds(test_case.cert_verification_time_seconds(), + &cert_verification_time)); uint64_t crl_verify_time = test_case.crl_verification_time_seconds(); DateTime crl_verification_time; - EXPECT_TRUE(ConvertTimeSeconds(crl_verify_time, &crl_verification_time)); + EXPECT_TRUE(DateTimeFromSeconds(crl_verify_time, &crl_verification_time)); if (crl_verify_time == 0) { crl_verification_time = cert_verification_time; } @@ -139,73 +120,57 @@ bool RunTest(const DeviceCertTest& test_case) { switch (test_case.expected_result()) { case PATH_VERIFICATION_FAILED: return TestVerifyCertificate(kResultFail, der_cert_path, - cert_verification_time, cast_trust_store); + cert_verification_time, + cast_trust_store.get()); case CRL_VERIFICATION_FAILED: return TestVerifyCRL(kResultFail, crl_bundle, crl_verification_time, - crl_trust_store); + crl_trust_store.get()); case REVOCATION_CHECK_FAILED_WITHOUT_CRL: return TestVerifyCertificate(kResultSuccess, der_cert_path, - cert_verification_time, cast_trust_store) && + cert_verification_time, + cast_trust_store.get()) && TestVerifyCRL(kResultFail, crl_bundle, crl_verification_time, - crl_trust_store) && - TestVerifyRevocation(CastCertError::kErrCrlInvalid, der_cert_path, - crl_bundle, crl_verification_time, - cert_verification_time, true, - cast_trust_store, crl_trust_store); + crl_trust_store.get()) && + TestVerifyRevocation( + CastCertError::kErrCrlInvalid, der_cert_path, crl_bundle, + crl_verification_time, cert_verification_time, true, + cast_trust_store.get(), crl_trust_store.get()); case CRL_EXPIRED_AFTER_INITIAL_VERIFICATION: // fallthrough case REVOCATION_CHECK_FAILED: return TestVerifyCertificate(kResultSuccess, der_cert_path, - cert_verification_time, cast_trust_store) && + cert_verification_time, + cast_trust_store.get()) && TestVerifyCRL(kResultSuccess, crl_bundle, crl_verification_time, - crl_trust_store) && - TestVerifyRevocation(CastCertError::kErrCertsRevoked, - der_cert_path, crl_bundle, - crl_verification_time, cert_verification_time, - false, cast_trust_store, crl_trust_store); + crl_trust_store.get()) && + TestVerifyRevocation( + CastCertError::kErrCertsRevoked, der_cert_path, crl_bundle, + crl_verification_time, cert_verification_time, true, + cast_trust_store.get(), crl_trust_store.get()); case SUCCESS: return (crl_bundle.empty() || TestVerifyCRL(kResultSuccess, crl_bundle, crl_verification_time, - crl_trust_store)) && + crl_trust_store.get())) && TestVerifyCertificate(kResultSuccess, der_cert_path, - cert_verification_time, cast_trust_store) && + cert_verification_time, + cast_trust_store.get()) && TestVerifyRevocation(CastCertError::kNone, der_cert_path, crl_bundle, crl_verification_time, cert_verification_time, !crl_bundle.empty(), - cast_trust_store, crl_trust_store); + cast_trust_store.get(), + crl_trust_store.get()); case UNSPECIFIED: return false; } return false; } -std::string ReadEntireFileToString(const std::string& filename) { - int fd = open(filename.c_str(), O_RDONLY); - if (fd == -1) { - return {}; - } - off_t file_size = lseek(fd, 0, SEEK_END); - lseek(fd, 0, SEEK_SET); - std::string contents(file_size, 0); - off_t bytes_read = 0; - while (bytes_read < file_size) { - int ret = read(fd, &contents[bytes_read], file_size - bytes_read); - if (ret == -1 && errno != EINTR) { - return {}; - } else { - bytes_read += ret; - } - } - close(fd); - - return contents; -} - // 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 = ReadEntireFileToString(test_suite_file_name); + std::string testsuite_raw = + testing::ReadEntireFileToString(test_suite_file_name); ASSERT_FALSE(testsuite_raw.empty()); DeviceCertTestSuite test_suite; ASSERT_TRUE(test_suite.ParseFromString(testsuite_raw)); diff --git a/cast/common/certificate/test_helpers.cc b/cast/common/certificate/test_helpers.cc index d64f10c4..fa04a2e9 100644 --- a/cast/common/certificate/test_helpers.cc +++ b/cast/common/certificate/test_helpers.cc @@ -8,10 +8,35 @@ #include <stdio.h> #include <string.h> +#include "platform/api/logging.h" + namespace cast { namespace certificate { namespace testing { +std::string ReadEntireFileToString(const std::string& filename) { + FILE* file = fopen(filename.c_str(), "r"); + if (file == nullptr) { + return {}; + } + fseek(file, 0, SEEK_END); + long file_size = ftell(file); + fseek(file, 0, SEEK_SET); + std::string contents(file_size, 0); + int bytes_read = 0; + while (bytes_read < file_size) { + size_t ret = fread(&contents[bytes_read], 1, file_size - bytes_read, file); + if (ret == 0 && ferror(file)) { + return {}; + } else { + bytes_read += ret; + } + } + fclose(file); + + return contents; +} + std::vector<std::string> ReadCertificatesFromPemFile( const std::string& filename) { FILE* fp = fopen(filename.c_str(), "r"); @@ -40,6 +65,66 @@ std::vector<std::string> ReadCertificatesFromPemFile( return certs; } +SignatureTestData::SignatureTestData() + : message{nullptr, 0}, sha1{nullptr, 0}, sha256{nullptr, 0} {} + +SignatureTestData::~SignatureTestData() { + OPENSSL_free(const_cast<uint8_t*>(message.data)); + OPENSSL_free(const_cast<uint8_t*>(sha1.data)); + OPENSSL_free(const_cast<uint8_t*>(sha256.data)); +} + +SignatureTestData ReadSignatureTestData(const std::string& filename) { + FILE* fp = fopen(filename.c_str(), "r"); + OSP_DCHECK(fp); + SignatureTestData result = {}; + for (;;) { + char* name; + char* header; + unsigned char* data; + long length; + if (PEM_read(fp, &name, &header, &data, &length) == 1) { + if (strcmp(name, "MESSAGE") == 0) { + OSP_DCHECK(!result.message.data); + result.message.data = data; + result.message.length = length; + } else if (strcmp(name, "SIGNATURE SHA1") == 0) { + OSP_DCHECK(!result.sha1.data); + result.sha1.data = data; + result.sha1.length = length; + } else if (strcmp(name, "SIGNATURE SHA256") == 0) { + OSP_DCHECK(!result.sha256.data); + result.sha256.data = data; + result.sha256.length = length; + } else { + OPENSSL_free(data); + } + OPENSSL_free(name); + OPENSSL_free(header); + } else { + break; + } + } + OSP_DCHECK(result.message.data); + OSP_DCHECK(result.sha1.data); + OSP_DCHECK(result.sha256.data); + + return result; +} + +std::unique_ptr<TrustStore> CreateTrustStoreFromPemFile( + const std::string& filename) { + std::unique_ptr<TrustStore> store = std::make_unique<TrustStore>(); + + std::vector<std::string> certs = + testing::ReadCertificatesFromPemFile(filename); + for (const auto& der_cert : certs) { + const uint8_t* data = (const uint8_t*)der_cert.data(); + store->certs.emplace_back(d2i_X509(nullptr, &data, der_cert.size())); + } + return store; +} + } // namespace testing } // namespace certificate } // namespace cast diff --git a/cast/common/certificate/test_helpers.h b/cast/common/certificate/test_helpers.h index 75a3301f..ac3a136d 100644 --- a/cast/common/certificate/test_helpers.h +++ b/cast/common/certificate/test_helpers.h @@ -8,13 +8,32 @@ #include <string> #include <vector> +#include "cast/common/certificate/cast_cert_validator_internal.h" +#include "cast/common/certificate/types.h" + namespace cast { namespace certificate { namespace testing { +std::string ReadEntireFileToString(const std::string& filename); std::vector<std::string> ReadCertificatesFromPemFile( const std::string& filename); +class SignatureTestData { + public: + SignatureTestData(); + ~SignatureTestData(); + + ConstDataSpan message; + ConstDataSpan sha1; + ConstDataSpan sha256; +}; + +SignatureTestData ReadSignatureTestData(const std::string& filename); + +std::unique_ptr<TrustStore> CreateTrustStoreFromPemFile( + const std::string& filename); + } // namespace testing } // namespace certificate } // namespace cast diff --git a/cast/common/certificate/types.cc b/cast/common/certificate/types.cc new file mode 100644 index 00000000..82d51358 --- /dev/null +++ b/cast/common/certificate/types.cc @@ -0,0 +1,70 @@ +// 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/common/certificate/types.h" + +#include "platform/api/logging.h" + +namespace cast { +namespace certificate { + +bool operator<(const DateTime& a, const DateTime& b) { + if (a.year < b.year) { + return true; + } else if (a.year > b.year) { + return false; + } + if (a.month < b.month) { + return true; + } else if (a.month > b.month) { + return false; + } + if (a.day < b.day) { + return true; + } else if (a.day > b.day) { + return false; + } + if (a.hour < b.hour) { + return true; + } else if (a.hour > b.hour) { + return false; + } + if (a.minute < b.minute) { + return true; + } else if (a.minute > b.minute) { + return false; + } + if (a.second < b.second) { + return true; + } else if (a.second > b.second) { + return false; + } + return false; +} + +bool operator>(const DateTime& a, const DateTime& b) { + return (b < a); +} + +bool DateTimeFromSeconds(uint64_t seconds, DateTime* time) { + struct tm tm = {}; + time_t sec = static_cast<time_t>(seconds); + OSP_DCHECK_GE(sec, 0); + OSP_DCHECK_EQ(static_cast<uint64_t>(sec), seconds); + if (!gmtime_r(&sec, &tm)) { + return false; + } + + time->second = tm.tm_sec; + time->minute = tm.tm_min; + time->hour = tm.tm_hour; + time->day = tm.tm_mday; + time->month = tm.tm_mon + 1; + time->year = tm.tm_year + 1900; + + return true; +} + +} // namespace certificate +} // namespace cast diff --git a/cast/common/certificate/types.h b/cast/common/certificate/types.h index 2691c691..62fe45ed 100644 --- a/cast/common/certificate/types.h +++ b/cast/common/certificate/types.h @@ -24,6 +24,10 @@ struct DateTime { uint8_t second; }; +bool operator<(const DateTime& a, const DateTime& b); +bool operator>(const DateTime& a, const DateTime& b); +bool DateTimeFromSeconds(uint64_t seconds, DateTime* time); + } // namespace certificate } // namespace cast 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; +} |