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