| // 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/testing/test_helpers.h" |
| #include "cast/common/channel/proto/cast_channel.pb.h" |
| #include "gtest/gtest.h" |
| #include "platform/api/time.h" |
| #include "platform/test/paths.h" |
| #include "testing/util/read_file.h" |
| #include "util/crypto/pem_helpers.h" |
| #include "util/osp_logging.h" |
| |
| namespace openscreen { |
| namespace cast { |
| |
| // TODO(crbug.com/openscreen/90): Remove these after Chromium is migrated to |
| // openscreen::cast |
| using DeviceCertTestSuite = ::cast::certificate::DeviceCertTestSuite; |
| using VerificationResult = ::cast::certificate::VerificationResult; |
| using DeviceCertTest = ::cast::certificate::DeviceCertTest; |
| |
| namespace { |
| |
| using ::cast::channel::AuthResponse; |
| |
| bool ConvertTimeSeconds(const 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; |
| } |
| |
| const std::string& GetSpecificTestDataPath() { |
| static std::string data_path = GetTestDataPath() + "cast/common/certificate/"; |
| return data_path; |
| } |
| |
| class CastAuthUtilTest : public ::testing::Test { |
| public: |
| CastAuthUtilTest() {} |
| ~CastAuthUtilTest() override {} |
| |
| void SetUp() override {} |
| |
| protected: |
| static AuthResponse CreateAuthResponse( |
| std::vector<uint8_t>* signed_data, |
| ::cast::channel::HashAlgorithm digest_algorithm) { |
| std::vector<std::string> chain = ReadCertificatesFromPemFile( |
| GetSpecificTestDataPath() + "certificates/chromecast_gen1.pem"); |
| OSP_CHECK(!chain.empty()); |
| |
| testing::SignatureTestData signatures = testing::ReadSignatureTestData( |
| GetSpecificTestDataPath() + "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 ::cast::channel::SHA1: |
| response.set_signature( |
| std::string(reinterpret_cast<const char*>(signatures.sha1.data), |
| signatures.sha1.length)); |
| break; |
| case ::cast::channel::SHA256: |
| response.set_signature( |
| std::string(reinterpret_cast<const char*>(signatures.sha256.data), |
| signatures.sha256.length)); |
| break; |
| } |
| *signed_data = std::vector<uint8_t>( |
| signatures.message.data, |
| 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]; } |
| |
| // Mangles a vector by inverting the first byte. |
| static void MangleData(std::vector<uint8_t>* data) { |
| (*data)[0] = ~(*data)[0]; |
| } |
| |
| const std::string& data_path_{GetSpecificTestDataPath()}; |
| }; |
| |
| // 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::vector<uint8_t> signed_data; |
| AuthResponse auth_response = |
| CreateAuthResponse(&signed_data, ::cast::channel::SHA256); |
| DateTime now = {}; |
| ASSERT_TRUE(DateTimeFromSeconds(GetWallTimeSinceUnixEpoch().count(), &now)); |
| ErrorOr<CastDeviceCertPolicy> result = |
| VerifyCredentialsForTest(auth_response, signed_data, |
| CRLPolicy::kCrlOptional, nullptr, nullptr, now); |
| EXPECT_TRUE(result); |
| EXPECT_EQ(CastDeviceCertPolicy::kUnrestricted, result.value()); |
| } |
| |
| TEST_F(CastAuthUtilTest, VerifyBadCA) { |
| std::vector<uint8_t> signed_data; |
| AuthResponse auth_response = |
| CreateAuthResponse(&signed_data, ::cast::channel::SHA256); |
| MangleString(auth_response.mutable_intermediate_certificate(0)); |
| ErrorOr<CastDeviceCertPolicy> result = |
| VerifyCredentials(auth_response, signed_data); |
| EXPECT_FALSE(result); |
| EXPECT_EQ(Error::Code::kErrCertsParse, result.error().code()); |
| } |
| |
| TEST_F(CastAuthUtilTest, VerifyBadClientAuthCert) { |
| std::vector<uint8_t> signed_data; |
| AuthResponse auth_response = |
| CreateAuthResponse(&signed_data, ::cast::channel::SHA256); |
| MangleString(auth_response.mutable_client_auth_certificate()); |
| ErrorOr<CastDeviceCertPolicy> result = |
| VerifyCredentials(auth_response, signed_data); |
| EXPECT_FALSE(result); |
| EXPECT_EQ(Error::Code::kErrCertsParse, result.error().code()); |
| } |
| |
| TEST_F(CastAuthUtilTest, VerifyBadSignature) { |
| std::vector<uint8_t> signed_data; |
| AuthResponse auth_response = |
| CreateAuthResponse(&signed_data, ::cast::channel::SHA256); |
| MangleString(auth_response.mutable_signature()); |
| ErrorOr<CastDeviceCertPolicy> result = |
| VerifyCredentials(auth_response, signed_data); |
| EXPECT_FALSE(result); |
| EXPECT_EQ(Error::Code::kCastV2SignedBlobsMismatch, result.error().code()); |
| } |
| |
| TEST_F(CastAuthUtilTest, VerifyEmptySignature) { |
| std::vector<uint8_t> signed_data; |
| AuthResponse auth_response = |
| CreateAuthResponse(&signed_data, ::cast::channel::SHA256); |
| auth_response.mutable_signature()->clear(); |
| ErrorOr<CastDeviceCertPolicy> result = |
| VerifyCredentials(auth_response, signed_data); |
| EXPECT_FALSE(result); |
| EXPECT_EQ(Error::Code::kCastV2SignatureEmpty, result.error().code()); |
| } |
| |
| TEST_F(CastAuthUtilTest, VerifyUnsupportedDigest) { |
| std::vector<uint8_t> signed_data; |
| AuthResponse auth_response = |
| CreateAuthResponse(&signed_data, ::cast::channel::SHA1); |
| DateTime now = {}; |
| ASSERT_TRUE(DateTimeFromSeconds(GetWallTimeSinceUnixEpoch().count(), &now)); |
| ErrorOr<CastDeviceCertPolicy> result = VerifyCredentialsForTest( |
| auth_response, signed_data, CRLPolicy::kCrlOptional, nullptr, nullptr, |
| now, true); |
| EXPECT_FALSE(result); |
| EXPECT_EQ(Error::Code::kCastV2DigestUnsupported, result.error().code()); |
| } |
| |
| TEST_F(CastAuthUtilTest, VerifyBackwardsCompatibleDigest) { |
| std::vector<uint8_t> signed_data; |
| AuthResponse auth_response = |
| CreateAuthResponse(&signed_data, ::cast::channel::SHA1); |
| DateTime now = {}; |
| ASSERT_TRUE(DateTimeFromSeconds(GetWallTimeSinceUnixEpoch().count(), &now)); |
| ErrorOr<CastDeviceCertPolicy> result = |
| VerifyCredentialsForTest(auth_response, signed_data, |
| CRLPolicy::kCrlOptional, nullptr, nullptr, now); |
| EXPECT_TRUE(result); |
| } |
| |
| TEST_F(CastAuthUtilTest, VerifyBadPeerCert) { |
| std::vector<uint8_t> signed_data; |
| AuthResponse auth_response = |
| CreateAuthResponse(&signed_data, ::cast::channel::SHA256); |
| MangleData(&signed_data); |
| ErrorOr<CastDeviceCertPolicy> result = |
| VerifyCredentials(auth_response, signed_data); |
| EXPECT_FALSE(result); |
| EXPECT_EQ(Error::Code::kCastV2SignedBlobsMismatch, result.error().code()); |
| } |
| |
| TEST_F(CastAuthUtilTest, VerifySenderNonceMatch) { |
| AuthContext context = AuthContext::Create(); |
| const Error result = context.VerifySenderNonce(context.nonce(), true); |
| EXPECT_TRUE(result.ok()); |
| } |
| |
| 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(Error::Code::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(Error::Code::kCastV2SenderNonceMismatch, result.error().code()); |
| } |
| |
| TEST_F(CastAuthUtilTest, VerifyTLSCertificateSuccess) { |
| std::vector<std::string> tls_cert_der = ReadCertificatesFromPemFile( |
| data_path_ + "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()); |
| DateTime not_before; |
| DateTime not_after; |
| ASSERT_TRUE(GetCertValidTimeRange(tls_cert, ¬_before, ¬_after)); |
| uint64_t x; |
| ASSERT_TRUE(ConvertTimeSeconds(not_before, &x)); |
| std::chrono::seconds s(x); |
| |
| const Error result = VerifyTLSCertificateValidity(tls_cert, s); |
| EXPECT_TRUE(result.ok()); |
| X509_free(tls_cert); |
| } |
| |
| TEST_F(CastAuthUtilTest, VerifyTLSCertificateTooEarly) { |
| std::vector<std::string> tls_cert_der = ReadCertificatesFromPemFile( |
| data_path_ + "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()); |
| DateTime not_before; |
| DateTime not_after; |
| ASSERT_TRUE(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(Error::Code::kCastV2TlsCertValidStartDateInFuture, |
| result.error().code()); |
| X509_free(tls_cert); |
| } |
| |
| TEST_F(CastAuthUtilTest, VerifyTLSCertificateTooLate) { |
| std::vector<std::string> tls_cert_der = ReadCertificatesFromPemFile( |
| data_path_ + "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()); |
| DateTime not_before; |
| DateTime not_after; |
| ASSERT_TRUE(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(Error::Code::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 DateTime& verification_time, |
| bool crl_required, |
| TrustStore* cast_trust_store, |
| 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); |
| |
| CRLPolicy crl_policy = CRLPolicy::kCrlRequired; |
| if (!crl_required && crl_bundle.empty()) |
| crl_policy = CRLPolicy::kCrlOptional; |
| ErrorOr<CastDeviceCertPolicy> result = VerifyCredentialsForTest( |
| response, std::vector<uint8_t>(), 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 DeviceCertTest& test_case) { |
| TrustStore crl_trust_store; |
| TrustStore cast_trust_store; |
| if (test_case.use_test_trust_anchors()) { |
| crl_trust_store = TrustStore::CreateInstanceFromPemFile( |
| GetSpecificTestDataPath() + "certificates/cast_crl_test_root_ca.pem"); |
| cast_trust_store = TrustStore::CreateInstanceFromPemFile( |
| GetSpecificTestDataPath() + "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. |
| 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(DateTimeFromSeconds(cert_verify_time, &verification_time)); |
| |
| std::string crl_bundle = test_case.crl_bundle(); |
| ErrorOr<CastDeviceCertPolicy> result(CastDeviceCertPolicy::kUnrestricted); |
| switch (test_case.expected_result()) { |
| case ::cast::certificate::PATH_VERIFICATION_FAILED: |
| result = |
| TestVerifyRevocation(certificate_chain, crl_bundle, verification_time, |
| false, &cast_trust_store, &cast_trust_store); |
| EXPECT_EQ(result.error().code(), |
| Error::Code::kCastV2CertNotSignedByTrustedCa); |
| return result.error().code() == |
| Error::Code::kCastV2CertNotSignedByTrustedCa; |
| case ::cast::certificate::CRL_VERIFICATION_FAILED: |
| // Fall-through intended. |
| case ::cast::certificate::REVOCATION_CHECK_FAILED_WITHOUT_CRL: |
| result = |
| TestVerifyRevocation(certificate_chain, crl_bundle, verification_time, |
| true, &cast_trust_store, &cast_trust_store); |
| EXPECT_EQ(result.error().code(), Error::Code::kErrCrlInvalid); |
| return result.error().code() == Error::Code::kErrCrlInvalid; |
| case ::cast::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 ::cast::certificate::REVOCATION_CHECK_FAILED: |
| result = |
| TestVerifyRevocation(certificate_chain, crl_bundle, verification_time, |
| true, &cast_trust_store, &cast_trust_store); |
| EXPECT_EQ(result.error().code(), Error::Code::kErrCertsRevoked); |
| return result.error().code() == Error::Code::kErrCertsRevoked; |
| case ::cast::certificate::SUCCESS: |
| result = |
| TestVerifyRevocation(certificate_chain, crl_bundle, verification_time, |
| false, &cast_trust_store, &cast_trust_store); |
| EXPECT_EQ(result.error().code(), Error::Code::kCastV2SignedBlobsMismatch); |
| return result.error().code() == Error::Code::kCastV2SignedBlobsMismatch; |
| case ::cast::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 = ReadEntireFileToString(test_suite_file_name); |
| 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 cast |
| } // namespace openscreen |