blob: 2fedb99aa9f0f58386eeecd92ee7c888518260ba [file] [log] [blame]
/*
* Copyright (C) 2020 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "config.h"
#if HAVE(UI_EVENT_ATTRIBUTION)
#import "HTTPServer.h"
#import "PlatformUtilities.h"
#import "TestNavigationDelegate.h"
#import "Utilities.h"
#import <WebKit/WKWebViewPrivate.h>
#import <WebKit/WKWebViewPrivateForTesting.h>
#import <WebKit/WKWebsiteDataStorePrivate.h>
#if HAVE(RSA_BSSA)
#include <Security/SecKeyPriv.h>
#include <WebCore/PrivateClickMeasurement.h>
extern "C" {
#include <corecrypto/cc_priv.h>
#include <corecrypto/ccrng.h>
#include <corecrypto/ccrsa.h>
#include <corecrypto/ccrsabssa.h>
}
#endif // HAVE(RSA_BSSA)
@interface MockEventAttribution : NSObject
@property (nonatomic, assign, readonly) uint8_t sourceIdentifier;
@property (nonatomic, copy, readonly) NSURL *destinationURL;
@property (nonatomic, copy, readonly) NSURL *reportEndpoint;
@property (nonatomic, copy, readonly) NSString *sourceDescription;
@property (nonatomic, copy, readonly) NSString *purchaser;
- (instancetype)initWithReportEndpoint:(NSURL *)reportEndpoint destinationURL:(NSURL *)destinationURL;
@end
@implementation MockEventAttribution
- (instancetype)initWithReportEndpoint:(NSURL *)reportEndpoint destinationURL:(NSURL *)destinationURL
{
if (!(self = [super init]))
return nil;
_sourceIdentifier = 42;
_destinationURL = destinationURL;
_reportEndpoint = reportEndpoint;
_sourceDescription = @"test source description";
_purchaser = @"test purchaser";
return self;
}
@end
namespace TestWebKitAPI {
TEST(EventAttribution, Basic)
{
bool done = false;
HTTPServer server([&done, connectionCount = 0] (Connection connection) mutable {
switch (++connectionCount) {
case 1:
connection.receiveHTTPRequest([connection] (Vector<char>&& request1) {
EXPECT_TRUE(strnstr(request1.data(), "GET /conversionRequestBeforeRedirect HTTP/1.1\r\n", request1.size()));
const char* redirect = "HTTP/1.1 302 Found\r\n"
"Location: /.well-known/private-click-measurement/trigger-attribution/12\r\n"
"Content-Length: 0\r\n\r\n";
connection.send(redirect, [connection] {
connection.receiveHTTPRequest([connection] (Vector<char>&& request2) {
EXPECT_TRUE(strnstr(request2.data(), "GET /.well-known/private-click-measurement/trigger-attribution/12 HTTP/1.1\r\n", request2.size()));
const char* response = "HTTP/1.1 200 OK\r\n"
"Content-Length: 0\r\n\r\n";
connection.send(response);
});
});
});
break;
case 2:
connection.receiveHTTPRequest([&done] (Vector<char>&& request3) {
request3.append('\0');
EXPECT_TRUE(strnstr(request3.data(), "POST / HTTP/1.1\r\n", request3.size()));
const char* bodyBegin = strnstr(request3.data(), "\r\n\r\n", request3.size()) + strlen("\r\n\r\n");
EXPECT_STREQ(bodyBegin, "{\"source_engagement_type\":\"click\",\"source_site\":\"127.0.0.1\",\"source_id\":42,\"attributed_on_site\":\"example.com\",\"trigger_data\":12,\"version\":2}");
done = true;
});
break;
}
}, HTTPServer::Protocol::Https);
NSURL *serverURL = server.request().URL;
auto exampleURL = [NSURL URLWithString:@"https://example.com/"];
auto attribution = adoptNS([[MockEventAttribution alloc] initWithReportEndpoint:server.request().URL destinationURL:exampleURL]);
auto webView = adoptNS([WKWebView new]);
webView.get()._uiEventAttribution = (UIEventAttribution *)attribution.get();
[[webView configuration].websiteDataStore _setResourceLoadStatisticsEnabled:YES];
[[webView configuration].websiteDataStore _allowTLSCertificateChain:@[(id)testCertificate().get()] forHost:serverURL.host];
[webView _setPrivateClickMeasurementAttributionReportURLsForTesting:serverURL destinationURL:exampleURL completionHandler:^{
[webView _setPrivateClickMeasurementOverrideTimerForTesting:YES completionHandler:^{
NSString *html = [NSString stringWithFormat:@"<script>fetch('%@conversionRequestBeforeRedirect',{mode:'no-cors'})</script>", serverURL];
[webView loadHTMLString:html baseURL:exampleURL];
}];
}];
Util::run(&done);
}
#if HAVE(RSA_BSSA)
TEST(EventAttribution, FraudPrevention)
{
bool done = false;
// Generate the server key pair.
size_t modulusNBits = 4096;
int error = 0;
struct ccrng_state* rng = ccrng(&error);
const uint8_t e[] = { 0x1, 0x00, 0x01 };
ccrsa_full_ctx_decl(ccn_sizeof(modulusNBits), rsaPrivateKey);
error = ccrsa_generate_key(modulusNBits, rsaPrivateKey, sizeof(e), e, rng);
ccrsa_pub_ctx_t rsaPublicKey = ccrsa_ctx_public(rsaPrivateKey);
size_t modulusNBytes = cc_ceiling(ccrsa_pubkeylength(rsaPublicKey), 8);
size_t exportSize = ccder_encode_rsa_pub_size(rsaPublicKey);
auto publicKey = adoptNS([[NSMutableData alloc] initWithLength:exportSize]);
ccder_encode_rsa_pub(rsaPublicKey, static_cast<uint8_t*>([publicKey mutableBytes]), static_cast<uint8_t*>([publicKey mutableBytes]) + [publicKey length]);
auto secKey = adoptCF(SecKeyCreateWithData((__bridge CFDataRef)publicKey.get(), (__bridge CFDictionaryRef)@{
(__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeRSA,
(__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPublic
}, nil));
auto spkiData = adoptCF(SecKeyCopySubjectPublicKeyInfo(secKey.get()));
auto *nsSpkiData = (__bridge NSData *)spkiData.get();
auto keyData = base64URLEncodeToVector(nsSpkiData.bytes, nsSpkiData.length);
// The server.
HTTPServer server([&done, connectionCount = 0, &rsaPrivateKey, &modulusNBytes, &rng, &keyData, &secKey] (Connection connection) mutable {
switch (++connectionCount) {
case 1:
connection.receiveHTTPRequest([connection, &rsaPrivateKey, &modulusNBytes, &rng, &keyData, &done, &secKey] (Vector<char>&& request1) {
EXPECT_TRUE(strnstr(request1.data(), "GET / HTTP/1.1\r\n", request1.size()));
// Example response: { "token_public_key": "ABCD" }. "ABCD" should be Base64URL encoded.
auto response = makeString("HTTP/1.1 200 OK\r\n"
"Content-Type: application/json\r\n"
"Content-Length: ", 24 + keyData.length(), "\r\n\r\n"
"{\"token_public_key\": \"", keyData, "\"}");
connection.send(WTFMove(response), [connection, &rsaPrivateKey, &modulusNBytes, &rng, &keyData, &done, &secKey] {
connection.receiveHTTPRequest([connection, &rsaPrivateKey, &modulusNBytes, &rng, &keyData, &done, &secKey] (Vector<char>&& request2) {
EXPECT_TRUE(strnstr(request2.data(), "POST / HTTP/1.1\r\n", request2.size()));
auto request2String = String(request2.data());
auto key = String("source_unlinkable_token");
auto start = request2String.find(key);
start += key.length() + 3;
auto end = request2String.find('"', start);
auto token = request2String.substring(start, end - start);
auto blindedMessage = base64URLDecode(token);
const struct ccrsabssa_ciphersuite *ciphersuite = &ccrsabssa_ciphersuite_rsa4096_sha384;
auto blindedSignature = adoptNS([[NSMutableData alloc] initWithLength:modulusNBytes]);
ccrsabssa_sign_blinded_message(ciphersuite, rsaPrivateKey, blindedMessage->data(), blindedMessage->size(), static_cast<uint8_t *>([blindedSignature mutableBytes]), [blindedSignature length], rng);
auto unlinkableToken = base64URLEncodeToString([blindedSignature bytes], [blindedSignature length]);
// Example response: { "unlinkable_token": "ABCD" }. "ABCD" should be Base64URL encoded.
auto response = makeString("HTTP/1.1 200 OK\r\n"
"Content-Type: application/json\r\n"
"Content-Length: ", 24 + unlinkableToken.length(), "\r\n\r\n"
"{\"unlinkable_token\": \"", unlinkableToken, "\"}");
connection.send(WTFMove(response), [connection, &keyData, &done, unlinkableToken, token, &secKey] {
connection.receiveHTTPRequest([connection, &keyData, &done, unlinkableToken, token, &secKey] (Vector<char>&& request3) {
EXPECT_TRUE(strnstr(request3.data(), "GET / HTTP/1.1\r\n", request3.size()));
// Example response: { "token_public_key": "ABCD" }. "ABCD" should be Base64URL encoded.
auto response = makeString("HTTP/1.1 200 OK\r\n"
"Content-Type: application/json\r\n"
"Content-Length: ", 24 + keyData.length(), "\r\n\r\n"
"{\"token_public_key\": \"", keyData, "\"}");
connection.send(WTFMove(response), [connection, &done, unlinkableToken, token, &secKey] {
connection.receiveHTTPRequest([connection, &done, unlinkableToken, token, &secKey] (Vector<char>&& request4) {
EXPECT_TRUE(strnstr(request4.data(), "POST / HTTP/1.1\r\n", request4.size()));
EXPECT_TRUE(strnstr(request4.data(), "{\"source_engagement_type\":\"click\",\"source_site\":\"127.0.0.1\",\"source_id\":42,\"attributed_on_site\":\"example.com\",\"trigger_data\":12,\"version\":2,",
request4.size()));
EXPECT_FALSE(strnstr(request4.data(), token.utf8().data(), request4.size()));
EXPECT_FALSE(strnstr(request4.data(), unlinkableToken.utf8().data(), request4.size()));
auto request4String = String(request4.data());
auto key = String("source_secret_token");
auto start = request4String.find(key);
start += key.length() + 3;
auto end = request4String.find('"', start);
auto token = request4String.substring(start, end - start);
auto tokenVector = base64URLDecode(token);
auto tokenData = adoptNS([[NSData alloc] initWithBytes:tokenVector->data() length:tokenVector->size()]);
key = String("source_secret_token_signature");
start = request4String.find(key);
start += key.length() + 3;
end = request4String.find('"', start);
auto signature = request4String.substring(start, end - start);
auto signatureVector = base64URLDecode(signature);
auto signatureData = adoptNS([[NSData alloc] initWithBytes:signatureVector->data() length:signatureVector->size()]);
EXPECT_TRUE(SecKeyVerifySignature(secKey.get(), kSecKeyAlgorithmRSASignatureMessagePSSSHA384, (__bridge CFDataRef)tokenData.get(), (__bridge CFDataRef)signatureData.get(), NULL));
done = true;
});
});
});
});
});
});
});
break;
case 2:
connection.receiveHTTPRequest([connection] (Vector<char>&& request1) {
EXPECT_TRUE(strnstr(request1.data(), "GET /conversionRequestBeforeRedirect HTTP/1.1\r\n", request1.size()));
const char* redirect = "HTTP/1.1 302 Found\r\n"
"Location: /.well-known/private-click-measurement/trigger-attribution/12\r\n"
"Content-Length: 0\r\n\r\n";
connection.send(redirect, [connection] {
connection.receiveHTTPRequest([connection] (Vector<char>&& request2) {
EXPECT_TRUE(strnstr(request2.data(), "GET /.well-known/private-click-measurement/trigger-attribution/12 HTTP/1.1\r\n", request2.size()));
const char* response = "HTTP/1.1 200 OK\r\n"
"Content-Length: 0\r\n\r\n";
connection.send(response);
});
});
});
break;
}
}, HTTPServer::Protocol::Https);
NSURL *serverURL = server.request().URL;
auto exampleURL = [NSURL URLWithString:@"https://example.com/"];
auto attribution = adoptNS([[MockEventAttribution alloc] initWithReportEndpoint:serverURL destinationURL:exampleURL]);
auto webView = adoptNS([WKWebView new]);
[webView _setUIEventAttributionForTesting:(UIEventAttribution *)attribution.get() withNonce:@"ABCDEFabcdef0123456789"];
[[webView configuration].websiteDataStore _setResourceLoadStatisticsEnabled:YES];
[[webView configuration].websiteDataStore _allowTLSCertificateChain:@[(id)testCertificate().get()] forHost:serverURL.host];
[webView _setPrivateClickMeasurementAttributionReportURLsForTesting:serverURL destinationURL:exampleURL completionHandler:^{
[webView _setPrivateClickMeasurementOverrideTimerForTesting:YES completionHandler:^{
[webView _setPrivateClickMeasurementAttributionTokenPublicKeyURLForTesting:serverURL completionHandler:^{
[webView _setPrivateClickMeasurementAttributionTokenSignatureURLForTesting:serverURL completionHandler:^{
NSString *html = [NSString stringWithFormat:@"<script>setTimeout(function(){ fetch('%@conversionRequestBeforeRedirect',{mode:'no-cors'}); }, 100);</script>", serverURL];
[webView loadHTMLString:html baseURL:exampleURL];
}];
}];
}];
}];
Util::run(&done);
}
#endif
} // namespace TestWebKitAPI
#endif // PLATFORM(IOS_FAMILY)