Skip to content

Commit 5746b2d

Browse files
authored
Reset App Attest key state if attestKey fails (#41)
1 parent 186edc2 commit 5746b2d

File tree

2 files changed

+94
-2
lines changed

2 files changed

+94
-2
lines changed

AppCheckCore/Sources/AppAttestProvider/GACAppAttestProvider.m

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,27 @@ - (void)getTokenWithLimitedUse:(BOOL)limitedUse
361361

362362
return [self attestKey:keyID challenge:challenge];
363363
})
364+
.recoverOn(self.queue,
365+
^id(NSError *error) {
366+
// If Apple rejected the key (DCErrorInvalidKey) then reset the attestation and
367+
// throw a specific error to signal retry (GACAppAttestRejectionError).
368+
NSError *underlyingError = error.userInfo[NSUnderlyingErrorKey];
369+
if (underlyingError && [underlyingError.domain isEqualToString:DCErrorDomain] &&
370+
underlyingError.code == DCErrorInvalidKey) {
371+
GACAppCheckLog(
372+
GACLoggerAppCheckMessageCodeAttestationRejected, GACAppCheckLogLevelDebug,
373+
@"App Attest invalid key; the existing attestation will be reset.");
374+
375+
// Reset the attestation.
376+
return [self resetAttestation].thenOn(self.queue, ^NSError *(id result) {
377+
// Throw the rejection error.
378+
return [[GACAppAttestRejectionError alloc] init];
379+
});
380+
}
381+
382+
// Otherwise just re-throw the error.
383+
return error;
384+
})
364385
.thenOn(self.queue,
365386
^FBLPromise<NSArray *> *(GACAppAttestKeyAttestationResult *result) {
366387
// 3. Exchange the attestation to FAC token and pass the results to the next step.

AppCheckCore/Tests/Unit/AppAttestProvider/GACAppAttestProviderTests.m

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@
1414
* limitations under the License.
1515
*/
1616

17+
#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppAttestProvider.h"
18+
19+
#import <DeviceCheck/DeviceCheck.h>
1720
#import <XCTest/XCTest.h>
1821

1922
#import <OCMock/OCMock.h>
2023
#import "FBLPromise+Testing.h"
2124

22-
#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppAttestProvider.h"
23-
2425
#import "AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAPIService.h"
2526
#import "AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAttestationResponse.h"
2627
#import "AppCheckCore/Sources/AppAttestProvider/GACAppAttestService.h"
@@ -626,6 +627,76 @@ - (void)testGetToken_WhenAttestationIsRejected_ThenAttestationIsResetAndRetriedO
626627
[self verifyAllMocks];
627628
}
628629

630+
- (void)testGetToken_WhenExistingKeyIsRejectedByApple_ThenAttestationIsResetAndRetriedOnce_Success {
631+
// 1. Expect GACAppAttestService.isSupported.
632+
[OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)];
633+
634+
// 2. Expect storage getAppAttestKeyID.
635+
NSString *existingKeyID = @"existingKeyID";
636+
OCMExpect([self.mockStorage getAppAttestKeyID])
637+
.andReturn([FBLPromise resolvedWith:existingKeyID]);
638+
639+
// 3. Expect a stored artifact to be requested.
640+
__auto_type rejectedPromise = [self rejectedPromiseWithError:[NSError errorWithDomain:self.name
641+
code:NSNotFound
642+
userInfo:nil]];
643+
OCMExpect([self.mockArtifactStorage getArtifactForKey:existingKeyID]).andReturn(rejectedPromise);
644+
645+
// 4. Expect random challenge to be requested.
646+
OCMExpect([self.mockAPIService getRandomChallenge])
647+
.andReturn([FBLPromise resolvedWith:self.randomChallenge]);
648+
649+
// 5. Expect the key to be attested with the challenge.
650+
NSError *attestationError = [NSError errorWithDomain:DCErrorDomain
651+
code:DCErrorInvalidKey
652+
userInfo:nil];
653+
id attestCompletionArg = [OCMArg invokeBlockWithArgs:[NSNull null], attestationError, nil];
654+
OCMExpect([self.mockAppAttestService attestKey:existingKeyID
655+
clientDataHash:self.randomChallengeHash
656+
completionHandler:attestCompletionArg]);
657+
658+
// 6. Stored attestation to be reset.
659+
[self expectAttestationReset];
660+
661+
// 7. Expect the App Attest key pair to be generated and attested.
662+
NSString *newKeyID = @"newKeyID";
663+
NSData *attestationData = [[NSUUID UUID].UUIDString dataUsingEncoding:NSUTF8StringEncoding];
664+
[self expectAppAttestKeyGeneratedAndAttestedWithKeyID:newKeyID attestationData:attestationData];
665+
666+
// 8. Expect exchange request to be sent.
667+
GACAppCheckToken *appCheckToken = [[GACAppCheckToken alloc] initWithToken:@"App Check Token"
668+
expirationDate:[NSDate date]];
669+
NSData *artifactData = [@"attestation artifact" dataUsingEncoding:NSUTF8StringEncoding];
670+
__auto_type attestKeyResponse =
671+
[[GACAppAttestAttestationResponse alloc] initWithArtifact:artifactData token:appCheckToken];
672+
OCMExpect([self.mockAPIService attestKeyWithAttestation:attestationData
673+
keyID:newKeyID
674+
challenge:self.randomChallenge
675+
limitedUse:NO])
676+
.andReturn([FBLPromise resolvedWith:attestKeyResponse]);
677+
678+
// 9. Expect the artifact received from Firebase backend to be saved.
679+
OCMExpect([self.mockArtifactStorage setArtifact:artifactData forKey:newKeyID])
680+
.andReturn([FBLPromise resolvedWith:artifactData]);
681+
682+
// 10. Call get token.
683+
XCTestExpectation *completionExpectation =
684+
[self expectationWithDescription:@"completionExpectation"];
685+
[self.provider
686+
getTokenWithCompletion:^(GACAppCheckToken *_Nullable token, NSError *_Nullable error) {
687+
[completionExpectation fulfill];
688+
689+
XCTAssertEqualObjects(token.token, appCheckToken.token);
690+
XCTAssertEqualObjects(token.expirationDate, appCheckToken.expirationDate);
691+
XCTAssertNil(error);
692+
}];
693+
694+
[self waitForExpectations:@[ completionExpectation ] timeout:0.5 enforceOrder:YES];
695+
696+
// 11. Verify mocks.
697+
[self verifyAllMocks];
698+
}
699+
629700
#pragma mark - FAC token refresh (assertion)
630701

631702
- (void)testGetToken_WhenKeyRegistered_Success {

0 commit comments

Comments
 (0)