blob: 4535515d31ef8933a73f255096f2ec3c4da1b8de [file] [log] [blame]
// 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