| // Copyright 2017 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 <Cronet/Cronet.h> |
| |
| #include "components/cronet/ios/test/cronet_test_base.h" |
| #include "components/cronet/ios/test/start_cronet.h" |
| #include "components/grpc_support/test/quic_test_server.h" |
| #include "net/base/mac/url_conversions.h" |
| #include "net/cert/mock_cert_verifier.h" |
| #include "net/test/cert_test_util.h" |
| #include "net/test/test_data_directory.h" |
| |
| #include "testing/gtest_mac.h" |
| |
| namespace { |
| const bool kIncludeSubdomains = true; |
| const bool kExcludeSubdomains = false; |
| const bool kSuccess = true; |
| const bool kError = false; |
| const std::string kServerCert = "quic-chain.pem"; |
| NSDate* const kDistantFuture = [NSDate distantFuture]; |
| } // namespace |
| |
| namespace cronet { |
| // Tests public-key-pinning functionality. |
| class PkpTest : public CronetTestBase { |
| protected: |
| void SetUp() override { |
| CronetTestBase::SetUp(); |
| server_host_ = [NSString stringWithCString:grpc_support::kTestServerHost |
| encoding:NSUTF8StringEncoding]; |
| server_domain_ = [NSString stringWithCString:grpc_support::kTestServerDomain |
| encoding:NSUTF8StringEncoding]; |
| |
| NSString* request_url_str = |
| [NSString stringWithCString:grpc_support::kTestServerSimpleUrl |
| encoding:NSUTF8StringEncoding]; |
| request_url_ = [NSURL URLWithString:request_url_str]; |
| |
| // Create a Cronet enabled NSURLSession. |
| NSURLSessionConfiguration* sessionConfig = |
| [NSURLSessionConfiguration defaultSessionConfiguration]; |
| [Cronet installIntoSessionConfiguration:sessionConfig]; |
| url_session_ = [NSURLSession sessionWithConfiguration:sessionConfig |
| delegate:delegate_ |
| delegateQueue:nil]; |
| |
| // Set mock cert verifier. |
| [Cronet setMockCertVerifierForTesting:CreateMockCertVerifier({kServerCert}, |
| YES)]; |
| } |
| |
| void TearDown() override { |
| // It is safe to call the shutdownForTesting method even if a test |
| // didn't call StartCronet(). |
| [Cronet shutdownForTesting]; |
| CronetTestBase::TearDown(); |
| } |
| |
| // Sends a request to a given URL, waits for the response and asserts that |
| // the response is either successful or containing an error depending on |
| // the value of the passed |expected_success| parameter. |
| void sendRequestAndAssertResult(NSURL* url, bool expected_success) { |
| NSURLSessionDataTask* dataTask = |
| [url_session_ dataTaskWithURL:request_url_]; |
| StartDataTaskAndWaitForCompletion(dataTask); |
| if (expected_success) { |
| ASSERT_TRUE(IsResponseSuccessful(dataTask)); |
| } else { |
| ASSERT_FALSE(IsResponseSuccessful(dataTask)); |
| ASSERT_FALSE(IsResponseCanceled(dataTask)); |
| } |
| } |
| |
| // Adds a given public-key-pin and starts a Cronet engine for testing. |
| void AddPkpAndStartCronet(NSString* host, |
| NSData* hash, |
| BOOL include_subdomains, |
| NSDate* expiration_date) { |
| [Cronet setEnablePublicKeyPinningBypassForLocalTrustAnchors:NO]; |
| NSSet* hashes = [NSSet setWithObject:hash]; |
| NSError* error; |
| BOOL success = [Cronet addPublicKeyPinsForHost:host |
| pinHashes:hashes |
| includeSubdomains:include_subdomains |
| expirationDate:(NSDate*)expiration_date |
| error:&error]; |
| CHECK(success); |
| CHECK(!error); |
| StartCronet(grpc_support::GetQuicTestServerPort()); |
| } |
| |
| // Returns an arbitrary public key hash that doesn't match with any test |
| // certificate. |
| static NSData* NonMatchingHash() { |
| const int length = 32; |
| std::string hash(length, '\077'); |
| return [NSData dataWithBytes:hash.c_str() length:length]; |
| } |
| |
| // Returns hash value that matches the hash of the public key certificate used |
| // for testing. |
| static NSData* MatchingHash() { |
| scoped_refptr<net::X509Certificate> cert = |
| net::ImportCertFromFile(net::GetTestCertsDirectory(), kServerCert); |
| net::HashValue hash_value; |
| CalculatePublicKeySha256(*cert, &hash_value); |
| CHECK_EQ(32ul, hash_value.size()); |
| return [NSData dataWithBytes:hash_value.data() length:hash_value.size()]; |
| } |
| |
| NSURLSession* url_session_; |
| NSURL* request_url_; // "https://test.example.com/simple.txt" |
| NSString* server_host_; // test.example.com |
| NSString* server_domain_; // example.com |
| }; // class PkpTest |
| |
| // Tests the case when a mismatching pin is set for some host that is |
| // different from the one the client wants to access. In that case the other |
| // host pinning policy should not be applied and the client is expected to |
| // receive the successful response with the response code 200. |
| TEST_F(PkpTest, TestSuccessIfPinSetForDifferentHost) { |
| AddPkpAndStartCronet(@"some-other-host.com", NonMatchingHash(), |
| kExcludeSubdomains, kDistantFuture); |
| ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kSuccess)); |
| } |
| |
| // Tests the case when the pin hash does not match. The client is expected to |
| // receive the error response. |
| TEST_F(PkpTest, TestErrorIfPinDoesNotMatch) { |
| AddPkpAndStartCronet(server_host_, NonMatchingHash(), kExcludeSubdomains, |
| kDistantFuture); |
| ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kError)); |
| } |
| |
| // Tests the case when the pin hash matches. The client is expected to |
| // receive the successful response with the response code 200. |
| TEST_F(PkpTest, TestSuccessIfPinMatches) { |
| AddPkpAndStartCronet(server_host_, MatchingHash(), kExcludeSubdomains, |
| kDistantFuture); |
| ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kSuccess)); |
| } |
| |
| TEST_F(PkpTest, TestBypass) { |
| [Cronet setEnablePublicKeyPinningBypassForLocalTrustAnchors:YES]; |
| |
| NSSet* hashes = [NSSet setWithObject:NonMatchingHash()]; |
| NSError* error; |
| BOOL success = [Cronet addPublicKeyPinsForHost:server_host_ |
| pinHashes:hashes |
| includeSubdomains:kExcludeSubdomains |
| expirationDate:(NSDate*)kDistantFuture |
| error:&error]; |
| |
| EXPECT_FALSE(success); |
| EXPECT_EQ([error code], CRNErrorUnsupportedConfig); |
| } |
| |
| // Tests the case when the pin hash does not match and the client accesses the |
| // subdomain of the configured PKP host with includeSubdomains flag set to true. |
| // The client is expected to receive the error response. |
| TEST_F(PkpTest, TestIncludeSubdomainsFlagEqualTrue) { |
| AddPkpAndStartCronet(server_domain_, NonMatchingHash(), kIncludeSubdomains, |
| kDistantFuture); |
| ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kError)); |
| } |
| |
| // Tests the case when the pin hash does not match and the client accesses the |
| // subdomain of the configured PKP host with includeSubdomains flag set to |
| // false. The client is expected to receive the successful response with the |
| // response code 200. |
| TEST_F(PkpTest, TestIncludeSubdomainsFlagEqualFalse) { |
| AddPkpAndStartCronet(server_domain_, NonMatchingHash(), kExcludeSubdomains, |
| kDistantFuture); |
| ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kSuccess)); |
| } |
| |
| // Tests a mismatching pin that will expire in 10 seconds. The pins should be |
| // still valid and enforced during the request; thus returning the pin match |
| // error. |
| TEST_F(PkpTest, TestSoonExpiringPin) { |
| AddPkpAndStartCronet(server_host_, NonMatchingHash(), kExcludeSubdomains, |
| [NSDate dateWithTimeIntervalSinceNow:10]); |
| ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kError)); |
| } |
| |
| // Tests mismatching pin that expired 1 second ago. Since the pin has |
| // expired, it should not be enforced during the request; thus a successful |
| // response is expected. |
| TEST_F(PkpTest, TestRecentlyExpiredPin) { |
| AddPkpAndStartCronet(server_host_, NonMatchingHash(), kExcludeSubdomains, |
| [NSDate dateWithTimeIntervalSinceNow:-1]); |
| ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kSuccess)); |
| } |
| |
| // Tests that host pinning is not persisted between multiple CronetEngine |
| // instances. |
| TEST_F(PkpTest, TestPinsAreNotPersisted) { |
| AddPkpAndStartCronet(server_host_, NonMatchingHash(), kExcludeSubdomains, |
| kDistantFuture); |
| ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kError)); |
| [Cronet shutdownForTesting]; |
| |
| // Restart Cronet engine and try the same request again. Since the pins are |
| // not persisted, a successful response is expected. |
| StartCronet(grpc_support::GetQuicTestServerPort()); |
| ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kSuccess)); |
| } |
| |
| // Tests that an error is returned when PKP hash size is not equal to 256 bits. |
| TEST_F(PkpTest, TestHashLengthError) { |
| [Cronet setEnablePublicKeyPinningBypassForLocalTrustAnchors:NO]; |
| char hash[31]; |
| NSData* shortHash = [NSData dataWithBytes:hash length:sizeof(hash)]; |
| NSSet* hashes = [NSSet setWithObject:shortHash]; |
| NSError* error; |
| BOOL success = [Cronet addPublicKeyPinsForHost:server_host_ |
| pinHashes:hashes |
| includeSubdomains:kExcludeSubdomains |
| expirationDate:kDistantFuture |
| error:&error]; |
| EXPECT_FALSE(success); |
| ASSERT_TRUE(error != nil); |
| EXPECT_STREQ([CRNCronetErrorDomain cStringUsingEncoding:NSUTF8StringEncoding], |
| [error.domain cStringUsingEncoding:NSUTF8StringEncoding]); |
| EXPECT_EQ(CRNErrorInvalidArgument, error.code); |
| EXPECT_TRUE([error.description rangeOfString:@"Invalid argument"].location != |
| NSNotFound); |
| EXPECT_TRUE([error.description rangeOfString:@"pinHashes"].location != |
| NSNotFound); |
| EXPECT_STREQ("pinHashes", [error.userInfo[CRNInvalidArgumentKey] |
| cStringUsingEncoding:NSUTF8StringEncoding]); |
| } |
| |
| // Tests that setting pins for the same host second time overrides the previous |
| // pins. |
| TEST_F(PkpTest, TestPkpOverrideNonMatchingToMatching) { |
| [Cronet setEnablePublicKeyPinningBypassForLocalTrustAnchors:NO]; |
| // Add non-matching pin. |
| BOOL success = |
| [Cronet addPublicKeyPinsForHost:server_host_ |
| pinHashes:[NSSet setWithObject:NonMatchingHash()] |
| includeSubdomains:kExcludeSubdomains |
| expirationDate:kDistantFuture |
| error:nil]; |
| ASSERT_TRUE(success); |
| // Add matching pin. |
| AddPkpAndStartCronet(server_host_, MatchingHash(), kExcludeSubdomains, |
| kDistantFuture); |
| ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kSuccess)); |
| } |
| |
| // Tests that setting pins for the same host second time overrides the previous |
| // pins. |
| TEST_F(PkpTest, TestPkpOverrideMatchingToNonMatching) { |
| [Cronet setEnablePublicKeyPinningBypassForLocalTrustAnchors:NO]; |
| // Add matching pin. |
| BOOL success = |
| [Cronet addPublicKeyPinsForHost:server_host_ |
| pinHashes:[NSSet setWithObject:MatchingHash()] |
| includeSubdomains:kExcludeSubdomains |
| expirationDate:kDistantFuture |
| error:nil]; |
| ASSERT_TRUE(success); |
| // Add non-matching pin. |
| AddPkpAndStartCronet(server_host_, NonMatchingHash(), kExcludeSubdomains, |
| kDistantFuture); |
| ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kError)); |
| } |
| |
| } // namespace cronet |