Skip to content

Commit d1957bd

Browse files
App Attest provider: attestation sequence (#7971)
* App Attest provider: attestation sequence (#761) * App Attest draft WIP * FIRAppAttestProvider initializers * ./scripts/style.sh * FIRAppAttestProvider implementation draft * Basic FIRAppAttestProviderTests and fixes * style * testGetTokenWhenAppAttestIsNotSupported * More FIRAppAttestProviderTests * Cleanup * Remove unused file * Availability annotations on DCAppAttestService category. * Guard FIRAppAttestProvider with #if TARGET_OS_IOS * Formatting * Fix SPM * app_check.yaml: Add diagnostics SPM builds * fix yaml * Fix Firebase-Package scheme bad merge * Fix typo * FIRAppAttestProvider: hide default init * FIRAppAttestKeyIDStorage: methods placeholders * Comments * Fix updated block definition
1 parent 08743d5 commit d1957bd

File tree

14 files changed

+971
-8
lines changed

14 files changed

+971
-8
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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+
17+
#import <Foundation/Foundation.h>
18+
19+
@class FBLPromise<Result>;
20+
@class FIRAppCheckToken;
21+
@protocol FIRAppCheckAPIServiceProtocol;
22+
23+
NS_ASSUME_NONNULL_BEGIN
24+
25+
@protocol FIRAppAttestAPIServiceProtocol <NSObject>
26+
27+
/// Request a random challenge from server.
28+
- (FBLPromise<NSData *> *)getRandomChallenge;
29+
30+
/// Exchanges attestation data to FAC token.
31+
- (FBLPromise<FIRAppCheckToken *> *)appCheckTokenWithAttestation:(NSData *)attestation
32+
keyID:(NSString *)keyID
33+
challenge:(NSData *)challenge;
34+
35+
@end
36+
37+
@interface FIRAppAttestAPIService : NSObject <FIRAppAttestAPIServiceProtocol>
38+
39+
- (instancetype)initWithAPIService:(id<FIRAppCheckAPIServiceProtocol>)APIService
40+
projectID:(NSString *)projectID
41+
appID:(NSString *)appID;
42+
43+
@end
44+
45+
NS_ASSUME_NONNULL_END
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
17+
#import "FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h"
18+
19+
#if __has_include(<FBLPromises/FBLPromises.h>)
20+
#import <FBLPromises/FBLPromises.h>
21+
#else
22+
#import "FBLPromises.h"
23+
#endif
24+
25+
#import "FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h"
26+
27+
@interface FIRAppAttestAPIService ()
28+
29+
@property(nonatomic, readonly) id<FIRAppCheckAPIServiceProtocol> APIService;
30+
31+
@property(nonatomic, readonly) NSString *projectID;
32+
@property(nonatomic, readonly) NSString *appID;
33+
34+
@end
35+
36+
@implementation FIRAppAttestAPIService
37+
38+
- (instancetype)initWithAPIService:(id<FIRAppCheckAPIServiceProtocol>)APIService
39+
projectID:(NSString *)projectID
40+
appID:(NSString *)appID {
41+
self = [super init];
42+
if (self) {
43+
_APIService = APIService;
44+
_projectID = projectID;
45+
_appID = appID;
46+
}
47+
return self;
48+
}
49+
50+
- (nonnull FBLPromise<FIRAppCheckToken *> *)
51+
appCheckTokenWithAttestation:(nonnull NSData *)attestation
52+
keyID:(nonnull NSString *)keyID
53+
challenge:(nonnull NSData *)challenge {
54+
// TODO: Implement.
55+
return [FBLPromise resolvedWith:nil];
56+
}
57+
58+
- (nonnull FBLPromise<NSData *> *)getRandomChallenge {
59+
// TODO: Implement.
60+
return [FBLPromise resolvedWith:nil];
61+
}
62+
63+
@end
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
17+
// Currently DCAppAttestService is available on iOS only.
18+
#if TARGET_OS_IOS
19+
20+
#import <DeviceCheck/DeviceCheck.h>
21+
22+
#import "FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestService.h"
23+
24+
NS_ASSUME_NONNULL_BEGIN
25+
26+
API_AVAILABLE(ios(14.0))
27+
API_UNAVAILABLE(macos, tvos, watchos)
28+
@interface DCAppAttestService (FIRAppAttestService) <FIRAppAttestService>
29+
30+
@end
31+
32+
NS_ASSUME_NONNULL_END
33+
34+
#endif // TARGET_OS_IOS
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
17+
#import "FirebaseAppCheck/Sources/AppAttestProvider/DCAppAttestService+FIRAppAttestService.h"
18+
19+
// Currently DCAppAttestService is available on iOS only.
20+
#if TARGET_OS_IOS
21+
22+
@implementation DCAppAttestService (FIRAppAttestService)
23+
24+
@end
25+
26+
#endif // TARGET_OS_IOS
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
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+
17+
#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppAttestProvider.h"
18+
19+
#import "FirebaseAppCheck/Sources/AppAttestProvider/DCAppAttestService+FIRAppAttestService.h"
20+
21+
#if __has_include(<FBLPromises/FBLPromises.h>)
22+
#import <FBLPromises/FBLPromises.h>
23+
#else
24+
#import "FBLPromises.h"
25+
#endif
26+
27+
#import "FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h"
28+
#import "FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestService.h"
29+
#import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h"
30+
#import "FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h"
31+
#import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h"
32+
33+
#import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
34+
35+
NS_ASSUME_NONNULL_BEGIN
36+
37+
/// A data object that contains all key attest data required for FAC token exchange.
38+
@interface FIRAppAttestKeyAttestationResult : NSObject
39+
40+
@property(nonatomic, readonly) NSString *keyID;
41+
@property(nonatomic, readonly) NSData *challenge;
42+
@property(nonatomic, readonly) NSData *attestation;
43+
44+
- (instancetype)initWithKeyID:(NSString *)keyID
45+
challenge:(NSData *)challenge
46+
attestation:(NSData *)attestation;
47+
48+
@end
49+
50+
@implementation FIRAppAttestKeyAttestationResult
51+
52+
- (instancetype)initWithKeyID:(NSString *)keyID
53+
challenge:(NSData *)challenge
54+
attestation:(NSData *)attestation {
55+
self = [super init];
56+
if (self) {
57+
_keyID = keyID;
58+
_challenge = challenge;
59+
_attestation = attestation;
60+
}
61+
return self;
62+
}
63+
64+
@end
65+
66+
@interface FIRAppAttestProvider ()
67+
68+
@property(nonatomic, readonly) id<FIRAppAttestAPIServiceProtocol> APIService;
69+
@property(nonatomic, readonly) id<FIRAppAttestService> appAttestService;
70+
@property(nonatomic, readonly) id<FIRAppAttestKeyIDStorageProtocol> keyIDStorage;
71+
72+
@property(nonatomic, readonly) dispatch_queue_t queue;
73+
74+
@end
75+
76+
@implementation FIRAppAttestProvider
77+
78+
- (instancetype)initWithAppAttestService:(id<FIRAppAttestService>)appAttestService
79+
APIService:(id<FIRAppAttestAPIServiceProtocol>)APIService
80+
keyIDStorage:(id<FIRAppAttestKeyIDStorageProtocol>)keyIDStorage {
81+
self = [super init];
82+
if (self) {
83+
_appAttestService = appAttestService;
84+
_APIService = APIService;
85+
_keyIDStorage = keyIDStorage;
86+
_queue = dispatch_queue_create("com.firebase.FIRAppAttestProvider", DISPATCH_QUEUE_SERIAL);
87+
}
88+
return self;
89+
}
90+
91+
- (nullable instancetype)initWithApp:(FIRApp *)app {
92+
#if TARGET_OS_IOS
93+
NSURLSession *URLSession = [NSURLSession
94+
sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]];
95+
96+
FIRAppAttestKeyIDStorage *keyIDStorage =
97+
[[FIRAppAttestKeyIDStorage alloc] initWithAppName:app.name appID:app.options.googleAppID];
98+
99+
FIRAppCheckAPIService *APIService =
100+
[[FIRAppCheckAPIService alloc] initWithURLSession:URLSession
101+
APIKey:app.options.APIKey
102+
projectID:app.options.projectID
103+
appID:app.options.googleAppID];
104+
105+
FIRAppAttestAPIService *appAttestAPIService =
106+
[[FIRAppAttestAPIService alloc] initWithAPIService:APIService
107+
projectID:app.options.projectID
108+
appID:app.options.googleAppID];
109+
110+
return [self initWithAppAttestService:DCAppAttestService.sharedService
111+
APIService:appAttestAPIService
112+
keyIDStorage:keyIDStorage];
113+
#else // TARGET_OS_IOS
114+
return nil;
115+
#endif // TARGET_OS_IOS
116+
}
117+
118+
#pragma mark - FIRAppCheckProvider
119+
120+
- (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_Nullable))handler {
121+
// 1. Check `DCAppAttestService.isSupported`.
122+
[self isAppAttestSupported]
123+
.thenOn(self.queue,
124+
^FBLPromise<NSArray *> *(id result) {
125+
return [FBLPromise onQueue:self.queue
126+
all:@[
127+
// 2. Request random challenge.
128+
[self.APIService getRandomChallenge],
129+
// 3. Get App Attest key ID.
130+
[self getAppAttestKeyIDGenerateIfNeeded]
131+
]];
132+
})
133+
.thenOn(self.queue,
134+
^FBLPromise<FIRAppAttestKeyAttestationResult *> *(NSArray *challengeAndKeyID) {
135+
// 4. Attest the key.
136+
NSData *challenge = challengeAndKeyID.firstObject;
137+
NSString *keyID = challengeAndKeyID.lastObject;
138+
139+
return [self attestKey:keyID challenge:challenge];
140+
})
141+
.thenOn(self.queue,
142+
^FBLPromise<FIRAppCheckToken *> *(FIRAppAttestKeyAttestationResult *result) {
143+
// 5. Exchange the attestation to FAC token.
144+
return [self.APIService appCheckTokenWithAttestation:result.attestation
145+
keyID:result.keyID
146+
challenge:result.challenge];
147+
})
148+
// 6. Call the handler with the result.
149+
.then(^FBLPromise *(FIRAppCheckToken *token) {
150+
handler(token, nil);
151+
return nil;
152+
})
153+
.catch(^(NSError *error) {
154+
handler(nil, error);
155+
});
156+
}
157+
158+
/// Returns a resolved promise if App Attest is supported and a rejected promise if it is not.
159+
- (FBLPromise<NSNull *> *)isAppAttestSupported {
160+
if (self.appAttestService.isSupported) {
161+
return [FBLPromise resolvedWith:[NSNull null]];
162+
} else {
163+
NSError *error = [FIRAppCheckErrorUtil unsupportedAttestationProvider:@"AppAttestProvider"];
164+
FBLPromise *rejectedPromise = [FBLPromise pendingPromise];
165+
[rejectedPromise reject:error];
166+
return rejectedPromise;
167+
}
168+
}
169+
170+
/// Retrieves or generates App Attest key associated with the Firebase app.
171+
- (FBLPromise<NSString *> *)getAppAttestKeyIDGenerateIfNeeded {
172+
return [self.keyIDStorage getAppAttestKeyID].recoverOn(self.queue,
173+
^FBLPromise<NSString *> *(NSError *error) {
174+
return [self generateAppAttestKey];
175+
});
176+
}
177+
178+
/// Generates and stores App Attest key associated with the Firebase app.
179+
- (FBLPromise<NSString *> *)generateAppAttestKey {
180+
return [FBLPromise onQueue:self.queue
181+
wrapObjectOrErrorCompletion:^(FBLPromiseObjectOrErrorCompletion _Nonnull handler) {
182+
[self.appAttestService generateKeyWithCompletionHandler:handler];
183+
}]
184+
.thenOn(self.queue, ^FBLPromise<NSString *> *(NSString *keyID) {
185+
return [self.keyIDStorage setAppAttestKeyID:keyID];
186+
});
187+
}
188+
189+
- (FBLPromise<FIRAppAttestKeyAttestationResult *> *)attestKey:(NSString *)keyID
190+
challenge:(NSData *)challenge {
191+
return [FBLPromise onQueue:self.queue
192+
do:^id _Nullable {
193+
return [challenge base64EncodedDataWithOptions:0];
194+
}]
195+
.thenOn(
196+
self.queue,
197+
^FBLPromise<NSData *> *(NSData *challengeHash) {
198+
return [FBLPromise onQueue:self.queue
199+
wrapObjectOrErrorCompletion:^(FBLPromiseObjectOrErrorCompletion _Nonnull handler) {
200+
[self.appAttestService attestKey:keyID
201+
clientDataHash:challengeHash
202+
completionHandler:handler];
203+
}];
204+
})
205+
.thenOn(self.queue, ^FBLPromise<FIRAppAttestKeyAttestationResult *> *(NSData *attestation) {
206+
FIRAppAttestKeyAttestationResult *result =
207+
[[FIRAppAttestKeyAttestationResult alloc] initWithKeyID:keyID
208+
challenge:challenge
209+
attestation:attestation];
210+
return [FBLPromise resolvedWith:result];
211+
});
212+
}
213+
214+
@end
215+
216+
NS_ASSUME_NONNULL_END

0 commit comments

Comments
 (0)