GACAppAttestAPIService.m 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. /*
  2. * Copyright 2021 Google LLC
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAPIService.h"
  17. #if __has_include(<FBLPromises/FBLPromises.h>)
  18. #import <FBLPromises/FBLPromises.h>
  19. #else
  20. #import "FBLPromises.h"
  21. #endif
  22. #import "AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAttestationResponse.h"
  23. #import "AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h"
  24. #import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h"
  25. #import <GoogleUtilities/GULURLSessionDataResponse.h>
  26. NS_ASSUME_NONNULL_BEGIN
  27. static NSString *const kRequestFieldArtifact = @"artifact";
  28. static NSString *const kRequestFieldAssertion = @"assertion";
  29. static NSString *const kRequestFieldAttestation = @"attestation_statement";
  30. static NSString *const kRequestFieldChallenge = @"challenge";
  31. static NSString *const kRequestFieldKeyID = @"key_id";
  32. static NSString *const kRequestFieldLimitedUse = @"limited_use";
  33. static NSString *const kExchangeAppAttestAssertionEndpoint = @"exchangeAppAttestAssertion";
  34. static NSString *const kExchangeAppAttestAttestationEndpoint = @"exchangeAppAttestAttestation";
  35. static NSString *const kGenerateAppAttestChallengeEndpoint = @"generateAppAttestChallenge";
  36. static NSString *const kContentTypeKey = @"Content-Type";
  37. static NSString *const kJSONContentType = @"application/json";
  38. static NSString *const kHTTPMethodPost = @"POST";
  39. @interface GACAppAttestAPIService ()
  40. @property(nonatomic, readonly) id<GACAppCheckAPIServiceProtocol> APIService;
  41. @property(nonatomic, readonly) NSString *resourceName;
  42. // TODO(andrewheard): Remove or refactor property when short-lived token feature is implemented.
  43. // When `YES`, forces a short-lived token with a 5 minute TTL.
  44. @property(nonatomic, readonly) BOOL limitedUse;
  45. @end
  46. @implementation GACAppAttestAPIService
  47. - (instancetype)initWithAPIService:(id<GACAppCheckAPIServiceProtocol>)APIService
  48. resourceName:(NSString *)resourceName
  49. limitedUse:(BOOL)limitedUse {
  50. self = [super init];
  51. if (self) {
  52. _APIService = APIService;
  53. _resourceName = [resourceName copy];
  54. _limitedUse = limitedUse;
  55. }
  56. return self;
  57. }
  58. #pragma mark - Assertion request
  59. - (FBLPromise<GACAppCheckToken *> *)getAppCheckTokenWithArtifact:(NSData *)artifact
  60. challenge:(NSData *)challenge
  61. assertion:(NSData *)assertion {
  62. NSURL *URL = [self URLForEndpoint:kExchangeAppAttestAssertionEndpoint];
  63. return [self HTTPBodyWithArtifact:artifact challenge:challenge assertion:assertion]
  64. .then(^FBLPromise<GULURLSessionDataResponse *> *(NSData *HTTPBody) {
  65. return [self.APIService sendRequestWithURL:URL
  66. HTTPMethod:kHTTPMethodPost
  67. body:HTTPBody
  68. additionalHeaders:@{kContentTypeKey : kJSONContentType}];
  69. })
  70. .then(^id _Nullable(GULURLSessionDataResponse *_Nullable response) {
  71. return [self.APIService appCheckTokenWithAPIResponse:response];
  72. });
  73. }
  74. #pragma mark - Random Challenge
  75. - (nonnull FBLPromise<NSData *> *)getRandomChallenge {
  76. NSURL *URL = [self URLForEndpoint:kGenerateAppAttestChallengeEndpoint];
  77. return [FBLPromise onQueue:[self backgroundQueue]
  78. do:^id _Nullable {
  79. return [self.APIService sendRequestWithURL:URL
  80. HTTPMethod:kHTTPMethodPost
  81. body:nil
  82. additionalHeaders:nil];
  83. }]
  84. .then(^id _Nullable(GULURLSessionDataResponse *_Nullable response) {
  85. return [self randomChallengeWithAPIResponse:response];
  86. });
  87. }
  88. #pragma mark - Challenge response parsing
  89. - (FBLPromise<NSData *> *)randomChallengeWithAPIResponse:(GULURLSessionDataResponse *)response {
  90. return [FBLPromise onQueue:[self backgroundQueue]
  91. do:^id _Nullable {
  92. NSError *error;
  93. NSData *randomChallenge =
  94. [self randomChallengeFromResponseBody:response.HTTPBody
  95. error:&error];
  96. return randomChallenge ?: error;
  97. }];
  98. }
  99. - (nullable NSData *)randomChallengeFromResponseBody:(NSData *)response error:(NSError **)outError {
  100. if (response.length <= 0) {
  101. GACAppCheckSetErrorToPointer(
  102. [GACAppCheckErrorUtil errorWithFailureReason:@"Empty server response body."], outError);
  103. return nil;
  104. }
  105. NSError *JSONError;
  106. NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:response
  107. options:0
  108. error:&JSONError];
  109. if (![responseDict isKindOfClass:[NSDictionary class]]) {
  110. GACAppCheckSetErrorToPointer([GACAppCheckErrorUtil JSONSerializationError:JSONError], outError);
  111. return nil;
  112. }
  113. NSString *challenge = responseDict[@"challenge"];
  114. if (![challenge isKindOfClass:[NSString class]]) {
  115. GACAppCheckSetErrorToPointer(
  116. [GACAppCheckErrorUtil appCheckTokenResponseErrorWithMissingField:@"challenge"], outError);
  117. return nil;
  118. }
  119. NSData *randomChallenge = [[NSData alloc] initWithBase64EncodedString:challenge options:0];
  120. return randomChallenge;
  121. }
  122. #pragma mark - Attestation request
  123. - (FBLPromise<GACAppAttestAttestationResponse *> *)attestKeyWithAttestation:(NSData *)attestation
  124. keyID:(NSString *)keyID
  125. challenge:(NSData *)challenge {
  126. NSURL *URL = [self URLForEndpoint:kExchangeAppAttestAttestationEndpoint];
  127. return [self HTTPBodyWithAttestation:attestation keyID:keyID challenge:challenge]
  128. .then(^FBLPromise<GULURLSessionDataResponse *> *(NSData *HTTPBody) {
  129. return [self.APIService sendRequestWithURL:URL
  130. HTTPMethod:kHTTPMethodPost
  131. body:HTTPBody
  132. additionalHeaders:@{kContentTypeKey : kJSONContentType}];
  133. })
  134. .thenOn(
  135. [self backgroundQueue], ^id _Nullable(GULURLSessionDataResponse *_Nullable URLResponse) {
  136. NSError *error;
  137. __auto_type response =
  138. [[GACAppAttestAttestationResponse alloc] initWithResponseData:URLResponse.HTTPBody
  139. requestDate:[NSDate date]
  140. error:&error];
  141. return response ?: error;
  142. });
  143. }
  144. #pragma mark - Request HTTP Body
  145. - (FBLPromise<NSData *> *)HTTPBodyWithArtifact:(NSData *)artifact
  146. challenge:(NSData *)challenge
  147. assertion:(NSData *)assertion {
  148. if (artifact.length <= 0 || challenge.length <= 0 || assertion.length <= 0) {
  149. FBLPromise *rejectedPromise = [FBLPromise pendingPromise];
  150. [rejectedPromise reject:[GACAppCheckErrorUtil
  151. errorWithFailureReason:@"Missing or empty request parameter."]];
  152. return rejectedPromise;
  153. }
  154. return [FBLPromise onQueue:[self backgroundQueue]
  155. do:^id {
  156. id JSONObject = @{
  157. kRequestFieldArtifact : [self base64StringWithData:artifact],
  158. kRequestFieldChallenge : [self base64StringWithData:challenge],
  159. kRequestFieldAssertion : [self base64StringWithData:assertion],
  160. kRequestFieldLimitedUse : @(self.limitedUse)
  161. };
  162. return [self HTTPBodyWithJSONObject:JSONObject];
  163. }];
  164. }
  165. - (FBLPromise<NSData *> *)HTTPBodyWithAttestation:(NSData *)attestation
  166. keyID:(NSString *)keyID
  167. challenge:(NSData *)challenge {
  168. if (attestation.length <= 0 || keyID.length <= 0 || challenge.length <= 0) {
  169. FBLPromise *rejectedPromise = [FBLPromise pendingPromise];
  170. [rejectedPromise reject:[GACAppCheckErrorUtil
  171. errorWithFailureReason:@"Missing or empty request parameter."]];
  172. return rejectedPromise;
  173. }
  174. return [FBLPromise onQueue:[self backgroundQueue]
  175. do:^id {
  176. id JSONObject = @{
  177. kRequestFieldKeyID : keyID,
  178. kRequestFieldAttestation : [self base64StringWithData:attestation],
  179. kRequestFieldChallenge : [self base64StringWithData:challenge],
  180. kRequestFieldLimitedUse : @(self.limitedUse)
  181. };
  182. return [self HTTPBodyWithJSONObject:JSONObject];
  183. }];
  184. }
  185. - (FBLPromise<NSData *> *)HTTPBodyWithJSONObject:(nonnull id)JSONObject {
  186. NSError *encodingError;
  187. NSData *payloadJSON = [NSJSONSerialization dataWithJSONObject:JSONObject
  188. options:0
  189. error:&encodingError];
  190. FBLPromise<NSData *> *HTTPBodyPromise = [FBLPromise pendingPromise];
  191. if (payloadJSON) {
  192. [HTTPBodyPromise fulfill:payloadJSON];
  193. } else {
  194. [HTTPBodyPromise reject:[GACAppCheckErrorUtil JSONSerializationError:encodingError]];
  195. }
  196. return HTTPBodyPromise;
  197. }
  198. #pragma mark - Helpers
  199. - (NSString *)base64StringWithData:(NSData *)data {
  200. return [data base64EncodedStringWithOptions:0];
  201. }
  202. - (NSURL *)URLForEndpoint:(NSString *)endpoint {
  203. NSString *URL = [[self class] URLWithBaseURL:self.APIService.baseURL
  204. resourceName:self.resourceName];
  205. return [NSURL URLWithString:[NSString stringWithFormat:@"%@:%@", URL, endpoint]];
  206. }
  207. + (NSString *)URLWithBaseURL:(NSString *)baseURL resourceName:(NSString *)resourceName {
  208. return [NSString stringWithFormat:@"%@/%@", baseURL, resourceName];
  209. }
  210. - (dispatch_queue_t)backgroundQueue {
  211. return dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
  212. }
  213. @end
  214. NS_ASSUME_NONNULL_END