]> git.saurik.com Git - apple/security.git/blob - SecExperiment/SecExperiment.m
Security-59306.41.2.tar.gz
[apple/security.git] / SecExperiment / SecExperiment.m
1 //
2 // SecExperiment.m
3 // Security
4 //
5
6 #include <xpc/xpc.h>
7 #include <os/log.h>
8 #include <Security/SecTrustPriv.h>
9 #include <uuid/uuid.h>
10
11 #define OS_OBJECT_HAVE_OBJC_SUPPORT 1
12
13 #define SEC_EXP_NULL_BAD_INPUT ((void *_Nonnull)NULL)
14 #define SEC_EXP_NULL_OUT_OF_MEMORY SEC_EXP_NULL_BAD_INPUT
15
16 #define SEC_EXP_NIL_BAD_INPUT ((void *_Nonnull)nil)
17 #define SEC_EXP_NIL_OUT_OF_MEMORY SEC_EXP_NIL_BAD_INPUT
18
19 #define SEC_EXP_CONCRETE_CLASS_NAME(external_type) SecExpConcrete_##external_type
20 #define SEC_EXP_CONCRETE_PREFIX_STR "SecExpConcrete_"
21
22 #define SEC_EXP_OBJECT_DECL_INTERNAL_OBJC(external_type) \
23 @class SEC_EXP_CONCRETE_CLASS_NAME(external_type); \
24 typedef SEC_EXP_CONCRETE_CLASS_NAME(external_type) *external_type##_t
25
26 #define SEC_EXP_OBJECT_IMPL_INTERNAL_OBJC_WITH_PROTOCOL_AND_VISBILITY(external_type, _protocol, visibility, ...) \
27 @protocol OS_OBJECT_CLASS(external_type) <_protocol> \
28 @end \
29 visibility \
30 @interface SEC_EXP_CONCRETE_CLASS_NAME(external_type) : NSObject<OS_OBJECT_CLASS(external_type)> \
31 _Pragma("clang diagnostic push") \
32 _Pragma("clang diagnostic ignored \"-Wobjc-interface-ivars\"") \
33 __VA_ARGS__ \
34 _Pragma("clang diagnostic pop") \
35 @end \
36 typedef int _useless_typedef_oio_##external_type
37
38 #define SEC_EXP_OBJECT_IMPL_INTERNAL_OBJC_WITH_PROTOCOL(external_type, _protocol, ...) \
39 SEC_EXP_OBJECT_IMPL_INTERNAL_OBJC_WITH_PROTOCOL_AND_VISBILITY(external_type, _protocol, ,__VA_ARGS__)
40
41 #define SEC_EXP_OBJECT_IMPL_INTERNAL_OBJC(external_type, ...) \
42 SEC_EXP_OBJECT_IMPL_INTERNAL_OBJC_WITH_PROTOCOL(external_type, NSObject, ##__VA_ARGS__)
43
44 #define SEC_EXP_OBJECT_IMPL_INTERNAL_OBJC_WITH_VISIBILITY(external_type, visibility, ...) \
45 SEC_EXP_OBJECT_IMPL_INTERNAL_OBJC_WITH_PROTOCOL_AND_VISBILITY(external_type, NSObject, visibility, ##__VA_ARGS__)
46
47 SEC_EXP_OBJECT_DECL_INTERNAL_OBJC(sec_experiment);
48
49 #define SEC_EXP_OBJECT_IMPL 1
50 #import "SecExperimentPriv.h"
51 #import "SecExperimentInternal.h"
52 #import "SecCFRelease.h"
53 #import <Foundation/Foundation.h>
54 #import <CoreFoundation/CFXPCBridge.h>
55
56 #define SEC_EXPERIMENT_SAMPLING_RATE 100.0
57 #define HASH_INITIAL_VALUE 0
58 #define HASH_MULTIPLIER 31
59
60 const char *kSecExperimentDefaultsDomain = "com.apple.security.experiment";
61 const char *kSecExperimentDefaultsDisableSampling = "disableSampling";
62 const char *kSecExperimentTLSMobileAssetConfig = "TLSConfig";
63
64 const NSString *SecExperimentMAPrefix = @"com.apple.MobileAsset.";
65
66 SEC_EXP_OBJECT_IMPL_INTERNAL_OBJC(sec_experiment,
67 {
68 const char *identifier;
69 bool sampling_disabled;
70 });
71
72 @implementation SEC_EXP_CONCRETE_CLASS_NAME(sec_experiment)
73
74 - (instancetype)initWithBundle:(const char *)bundle
75 {
76 if (bundle == NULL) {
77 return SEC_EXP_NIL_BAD_INPUT;
78 }
79
80 self = [super init];
81 if (self == nil) {
82 return SEC_EXP_NIL_OUT_OF_MEMORY;
83 } else {
84 self->identifier = bundle;
85 }
86 return self;
87 }
88
89 // Computes hash of input and returns a value between 1-100
90 static uint32_t
91 _hash_multiplicative(const char *key, size_t len)
92 {
93 if (!key) {
94 return 0;
95 }
96 uint32_t hash = HASH_INITIAL_VALUE;
97 for (uint32_t i = 0; i < len; ++i) {
98 hash = HASH_MULTIPLIER * hash + key[i];
99 }
100 return hash % 101; // value between 1-100
101 }
102
103 // Computes hash of device UUID
104 static uint32_t
105 _get_host_id_hash(void)
106 {
107 static uuid_string_t hostuuid = {};
108 static uint32_t hash = 0;
109 static dispatch_once_t onceToken = 0;
110 dispatch_once(&onceToken, ^{
111 struct timespec timeout = {0, 0};
112 uuid_t uuid = {};
113 if (gethostuuid(uuid, &timeout) == 0) {
114 uuid_unparse(uuid, hostuuid);
115 hash = _hash_multiplicative(hostuuid, strlen(hostuuid));
116 } else {
117 onceToken = 0;
118 }
119 });
120 return hash;
121 }
122
123 static bool
124 sec_experiment_is_sampling_disabled_with_default(bool default_value)
125 {
126 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
127 if (defaults != nil) {
128 NSMutableDictionary *experimentDefaults = [[defaults persistentDomainForName:[NSString stringWithUTF8String:kSecExperimentDefaultsDomain]] mutableCopy];
129 if (experimentDefaults != nil) {
130 NSString *key = [NSString stringWithUTF8String:kSecExperimentDefaultsDisableSampling];
131 if (experimentDefaults[key] != nil) {
132 return [experimentDefaults[key] boolValue];
133 }
134 }
135 }
136
137 return default_value;
138 }
139
140 sec_experiment_t
141 sec_experiment_create(const char *bundle)
142 {
143 return [[SEC_EXP_CONCRETE_CLASS_NAME(sec_experiment) alloc] initWithBundle:bundle];
144 }
145
146 static xpc_object_t
147 _copy_builtin_experiment_asset(sec_experiment_t experiment)
148 {
149 if (strncmp(experiment->identifier, kSecExperimentTLSMobileAssetConfig, strlen(kSecExperimentTLSMobileAssetConfig)) != 0) {
150 return nil;
151 }
152
153 static NSDictionary *defaultTLSConfig = NULL;
154 static dispatch_once_t onceToken = 0;
155 dispatch_once(&onceToken, ^{
156 NSDictionary *validate = @{
157 @"tcp" : @{},
158 @"tls" : @{@"max_version": @0x0303,
159 @"false_start_enabled" : @false
160 }
161 };
162 NSDictionary *transform = @{
163 @"tcp" : @{},
164 @"tls" : @{@"max_version": @0x0304,
165 @"false_start_enabled" : @true
166 }
167 };
168 defaultTLSConfig = @{
169 @"validate" : validate,
170 @"transform" : transform
171 };
172 });
173
174 return _CFXPCCreateXPCObjectFromCFObject((__bridge CFDictionaryRef)defaultTLSConfig);
175 }
176
177 // Default check to compute sampling in lieu of MobileAsset download
178 static bool
179 _device_is_in_experiment(sec_experiment_t experiment)
180 {
181 if (experiment->sampling_disabled) {
182 return YES;
183 }
184
185 uint32_t sample = arc4random();
186 return (float)sample < ((float)UINT32_MAX / SEC_EXPERIMENT_SAMPLING_RATE);
187 }
188
189 static NSDictionary *
190 _copy_experiment_asset(sec_experiment_t experiment)
191 {
192 CFErrorRef error = NULL;
193 NSDictionary *config = NULL;
194 NSDictionary *asset = CFBridgingRelease(SecTrustOTASecExperimentCopyAsset(&error));
195 if (asset) {
196 config = [asset valueForKey:[NSString stringWithUTF8String:experiment->identifier]];
197 }
198 return config;
199 }
200
201 static xpc_object_t
202 _copy_defaults_experiment_asset(sec_experiment_t experiment)
203 {
204 xpc_object_t result = nil;
205
206 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
207 if (defaults != nil) {
208 NSMutableDictionary *experimentDefaults = [[defaults persistentDomainForName:[NSString stringWithUTF8String:kSecExperimentDefaultsDomain]] mutableCopy];
209 if (experimentDefaults != nil) {
210 NSString *key = [NSString stringWithUTF8String:experiment->identifier];
211 if (experimentDefaults[key] != nil) {
212 result = _CFXPCCreateXPCObjectFromCFObject((__bridge CFDictionaryRef)experimentDefaults[key]);
213 }
214 }
215 }
216
217 return result;
218 }
219
220 void
221 sec_experiment_set_sampling_disabled(sec_experiment_t experiment, bool sampling_disabled)
222 {
223 experiment->sampling_disabled = sampling_disabled;
224 }
225
226 xpc_object_t
227 sec_experiment_copy_configuration(sec_experiment_t experiment)
228 {
229 if (experiment == NULL) {
230 return NULL;
231 }
232 /* Check first for defaults configured */
233 if (!sec_experiment_is_sampling_disabled_with_default(experiment->sampling_disabled)) {
234 xpc_object_t defaultAsset = _copy_defaults_experiment_asset(experiment);
235 if (defaultAsset != nil) {
236 return defaultAsset;
237 }
238 }
239 /* Copy assets downloaded from MA */
240 NSDictionary *asset = _copy_experiment_asset(experiment);
241 if (asset != NULL) {
242 /* Get random config from array of experiments */
243 NSArray *array = [asset valueForKey:@"ConfigArray"];
244 if (array == NULL) {
245 return NULL;
246 }
247 NSDictionary *randomConfig = [array objectAtIndex:(arc4random() % [array count])];
248
249 /* Only if sampling is enabled for the experiment */
250 if (!experiment->sampling_disabled) {
251 /* Check FleetSampleRate if device should be in experiment */
252 uint32_t fleetSample = [[randomConfig objectForKey:@"FleetSampleRate"] intValue];
253
254 /* fleetSample is a percentage value configured to determine
255 percentage of devices in an experiment */
256 uint32_t hostIdHash = _get_host_id_hash();
257 if ((hostIdHash == 0) || (fleetSample < hostIdHash)) {
258 return nil;
259 }
260 /* Check device sampling rate if device should run experiment */
261 uint32_t samplingRate = [[randomConfig objectForKey:@"DeviceSampleRate"] intValue];
262 /* Only run experiment 1 out of the samplingRate value */
263 uint32_t sample = arc4random();
264 if ((float)sample > ((float)UINT32_MAX / samplingRate)) {
265 return nil;
266 }
267 }
268 return _CFXPCCreateXPCObjectFromCFObject((__bridge CFDictionaryRef)randomConfig);
269 }
270
271 /* If asset download is not successful, fallback to built-in */
272 if (_device_is_in_experiment(experiment)) {
273 return _copy_builtin_experiment_asset(experiment);
274 }
275 return nil;
276 }
277
278 const char *
279 sec_experiment_get_identifier(sec_experiment_t experiment)
280 {
281 return experiment->identifier;
282 }
283
284 bool
285 sec_experiment_run_internal(const char *experiment_name, bool sampling_disabled, dispatch_queue_t queue, sec_experiment_run_block_t run_block)
286 {
287 if (experiment_name == NULL || queue == nil || run_block == nil) {
288 return false;
289 }
290
291 dispatch_async(queue, ^{
292 sec_experiment_t experiment = sec_experiment_create(experiment_name);
293 if (experiment != nil) {
294 sec_experiment_set_sampling_disabled(experiment, sec_experiment_is_sampling_disabled_with_default(sampling_disabled));
295 xpc_object_t config = sec_experiment_copy_configuration(experiment);
296 if (config != nil) {
297 const char *identifier = sec_experiment_get_identifier(experiment);
298 if (run_block(identifier, config)) {
299 os_log_info(OS_LOG_DEFAULT, "Configuration '%s' for experiment '%s' succeeded", identifier, experiment_name);
300 } else {
301 os_log_info(OS_LOG_DEFAULT, "Configuration '%s' for experiment '%s' failed", identifier, experiment_name);
302 }
303 } else {
304 os_log_debug(OS_LOG_DEFAULT, "Experiment '%s' not sampled to run", experiment_name);
305 }
306 } else {
307 os_log_debug(OS_LOG_DEFAULT, "Experiment '%s' not found", experiment_name);
308 }
309 });
310
311 return true;
312 }
313
314 bool
315 sec_experiment_run(const char *experiment_name, dispatch_queue_t queue, sec_experiment_run_block_t run_block)
316 {
317 // Sampling is always enabled for SecExperiment callers. Appliations may override this by setting the
318 // `disableSampling` key in the `com.apple.security.experiment` defaults domain.
319 return sec_experiment_run_internal(experiment_name, false, queue, run_block);
320 }
321
322 @end