xref: /expo/ios/Exponent/Kernel/AppLoader/EXApiUtil.m (revision cd4bd26b)
1// Copyright 2015-present 650 Industries. All rights reserved.
2
3#import "EXApiUtil.h"
4
5#import <CommonCrypto/CommonDigest.h>
6#import <React/RCTUtils.h>
7
8#import "EXCachedResource.h"
9
10NS_ASSUME_NONNULL_BEGIN
11
12static NSString* kPublicKeyTag = @"exp.host.publickey";
13
14@implementation EXApiUtil
15
16+ (NSURL *)bundleUrlFromManifest:(EXUpdatesRawManifest *)manifest
17{
18  return [[self class] encodedUrlFromString:manifest.bundleUrl];
19}
20
21+ (NSURL *)encodedUrlFromString:(NSString *)urlString
22{
23  NSURL *url = [NSURL URLWithString:urlString];
24  if (!url) {
25    url = [NSURL URLWithString:[urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]];
26  }
27  return url;
28}
29
30+ (void)verifySignatureWithPublicKeyUrl:(NSURL *)publicKeyUrl
31                                   data:(NSString *)data
32                              signature:(NSString *)signature
33                           successBlock:(EXVerifySignatureSuccessBlock)successBlock
34                             errorBlock:(EXVerifySignatureErrorBlock)errorBlock
35{
36  [self fetchAndVerifySignatureWithPublicKeyUrl:publicKeyUrl
37                                           data:data
38                                      signature:signature
39                                       useCache:YES
40                                   successBlock:successBlock
41                                     errorBlock:errorBlock];
42}
43
44+ (void)fetchAndVerifySignatureWithPublicKeyUrl:(NSURL *)publicKeyUrl
45                                           data:(NSString *)data
46                                      signature:(NSString *)signature
47                                       useCache:(BOOL)useCache
48                                   successBlock:(EXVerifySignatureSuccessBlock)successBlock
49                                     errorBlock:(EXVerifySignatureErrorBlock)errorBlock
50{
51  if (!data || !signature) {
52    errorBlock([NSError errorWithDomain:@"EXAPIUtilDomain" code:-1 userInfo:@{ NSLocalizedDescriptionKey: @"Cannot verify the manifest because it is empty or has no signature." }]);
53    return;
54  }
55  EXCachedResource *publicKeyResource = [[EXCachedResource alloc] initWithResourceName:@"manifestPublicKey"
56                                                                          resourceType:@"pem"
57                                                                             remoteUrl:publicKeyUrl
58                                                                             cachePath:nil];
59  EXCachedResourceBehavior cacheBehavior = useCache ? EXCachedResourceUseCacheImmediately : EXCachedResourceNoCache;
60  [publicKeyResource loadResourceWithBehavior:cacheBehavior progressBlock:nil successBlock:^(NSData *publicKeyData) {
61    dispatch_async(dispatch_get_main_queue(), ^{
62      SecKeyRef publicKey = [self keyRefFromPEMData:publicKeyData];
63
64      NSData *signatureData = [[NSData alloc] initWithBase64EncodedString:signature options:0];
65      NSData *signedData = [data dataUsingEncoding:NSUTF8StringEncoding];
66
67      BOOL isValid = NO;
68      if (publicKey) {
69        isValid = [self verifyRSASHA256SignedData:signedData signatureData:signatureData publicKey:publicKey];
70        CFRelease(publicKey);
71      }
72      if (!isValid && useCache) {
73        [self fetchAndVerifySignatureWithPublicKeyUrl:publicKeyUrl
74                                                 data:data
75                                            signature:signature
76                                             useCache:NO
77                                         successBlock:successBlock
78                                           errorBlock:errorBlock];
79      } else {
80        successBlock(isValid);
81      }
82    });
83  } errorBlock:^(NSError *error) {
84    errorBlock(error);
85  }];
86}
87
88/**
89 *  Returns a CFRef to a SecKey given the raw pem data.
90 *  The CFRef should be CFReleased when you're finished.
91 *
92 *  Here is the Apple doc for this black hole:
93 *  https://developer.apple.com/library/prerelease/content/documentation/Security/Conceptual/CertKeyTrustProgGuide/iPhone_Tasks/iPhone_Tasks.html#//apple_ref/doc/uid/TP40001358-CH208-SW13
94 */
95+ (_Nullable SecKeyRef)keyRefFromPEMData:(NSData *)pemData
96{
97  NSString *pemString = [[NSString alloc] initWithData:pemData encoding:NSUTF8StringEncoding];
98
99  NSString *key = [NSString string];
100  NSArray<NSString *> *keyLines = [pemString componentsSeparatedByString:@"\n"];
101  BOOL foundKey = NO;
102
103  for (NSString *line in keyLines) {
104    if ([line isEqualToString:@"-----BEGIN PUBLIC KEY-----"]) {
105      foundKey = YES;
106    } else if ([line isEqualToString:@"-----END PUBLIC KEY-----"]) {
107      foundKey = NO;
108    } else if (foundKey) {
109      key = [key stringByAppendingString:line];
110    }
111  }
112
113  if (key.length == 0) {
114    return nil;
115  }
116
117  NSData *keyData = [[NSData alloc] initWithBase64EncodedString:key options:0];
118  if (keyData == nil) {
119    return nil;
120  }
121
122  NSData *tag = [NSData dataWithBytes:[kPublicKeyTag UTF8String] length:[kPublicKeyTag length]];
123
124  // Delete any old lingering key with the same tag.
125  NSDictionary *deleteParams = @{
126                                 (__bridge id)kSecClass: (__bridge id)kSecClassKey,
127                                 (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeRSA,
128                                 (__bridge id)kSecAttrApplicationTag: tag,
129                                 };
130  OSStatus secStatus = SecItemDelete((CFDictionaryRef)deleteParams);
131
132  SecKeyRef savedKeyRef = nil;
133
134  // Add key to system keychain.
135  NSDictionary *saveParams = @{
136                             (__bridge id)kSecClass: (__bridge id) kSecClassKey,
137                             (__bridge id)kSecAttrKeyType: (__bridge id) kSecAttrKeyTypeRSA,
138                             (__bridge id)kSecAttrApplicationTag: tag,
139                             (__bridge id)kSecAttrKeyClass: (__bridge id) kSecAttrKeyClassPublic,
140                             (__bridge id)kSecReturnPersistentRef: (__bridge id)kCFBooleanTrue,
141                             (__bridge id)kSecValueData: keyData,
142                             (__bridge id)kSecAttrKeySizeInBits: [NSNumber numberWithUnsignedInteger:keyData.length],
143                             (__bridge id)kSecAttrEffectiveKeySize: [NSNumber numberWithUnsignedInteger:keyData.length],
144                             (__bridge id)kSecAttrCanDerive: (__bridge id) kCFBooleanFalse,
145                             (__bridge id)kSecAttrCanEncrypt: (__bridge id) kCFBooleanTrue,
146                             (__bridge id)kSecAttrCanDecrypt: (__bridge id) kCFBooleanFalse,
147                             (__bridge id)kSecAttrCanVerify: (__bridge id) kCFBooleanTrue,
148                             (__bridge id)kSecAttrCanSign: (__bridge id) kCFBooleanFalse,
149                             (__bridge id)kSecAttrCanWrap: (__bridge id) kCFBooleanTrue,
150                             (__bridge id)kSecAttrCanUnwrap: (__bridge id) kCFBooleanFalse,
151                             };
152
153  secStatus = SecItemAdd((CFDictionaryRef)saveParams, (CFTypeRef *)&savedKeyRef);
154
155  if (savedKeyRef != nil) {
156    CFRelease(savedKeyRef);
157  }
158
159  if (secStatus != noErr && secStatus != errSecDuplicateItem) {
160    return nil;
161  }
162
163  // Fetch the SecKeyRef version of the key.
164  // note that kSecAttrKeyClass: kSecAttrKeyClassPublic doesn't seem to be required here.
165  // also: this doesn't work on iOS < 10.0
166  SecKeyRef keyRef = nil;
167  NSDictionary *queryParams = @{
168                              (__bridge id)kSecClass: (__bridge id) kSecClassKey,
169                              (__bridge id)kSecAttrKeyType: (__bridge id) kSecAttrKeyTypeRSA,
170                              (__bridge id)kSecAttrApplicationTag: tag,
171                              (__bridge id)kSecReturnRef: (__bridge id) kCFBooleanTrue,
172                              };
173  secStatus = SecItemCopyMatching((CFDictionaryRef)queryParams, (CFTypeRef *)&keyRef);
174
175  if (secStatus != noErr) {
176    return nil;
177  }
178
179  return keyRef;
180}
181
182+ (BOOL)verifyRSASHA256SignedData:(NSData *)signedData signatureData:(NSData *)signatureData publicKey:(_Nullable SecKeyRef)publicKey
183{
184  if (!publicKey) {
185    return NO;
186  }
187
188  uint8_t hashBytes[CC_SHA256_DIGEST_LENGTH];
189  if (!CC_SHA256([signedData bytes], (CC_LONG)[signedData length], hashBytes)) {
190    return NO;
191  }
192
193  OSStatus status = SecKeyRawVerify(publicKey,
194                                    kSecPaddingPKCS1SHA256,
195                                    hashBytes,
196                                    CC_SHA256_DIGEST_LENGTH,
197                                    [signatureData bytes],
198                                    [signatureData length]);
199
200  return status == errSecSuccess;
201}
202
203@end
204
205NS_ASSUME_NONNULL_END
206