Skip to content

Commit 1317f08

Browse files
authored
[App Check] Reset App Attest key state if attestKey fails (#11986)
1 parent d5caf45 commit 1317f08

File tree

3 files changed

+94
-0
lines changed

3 files changed

+94
-0
lines changed

FirebaseAppCheck/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Unreleased
2+
- [fixed] Added invalid key error handling in App Attest key attestation. (#11986)
3+
14
# 10.17.0
25
- [fixed] Replaced semantic imports (`@import FirebaseAppCheckInterop`) with umbrella header imports
36
(`#import <FirebaseAppCheckInterop/FirebaseAppCheckInterop.h>`) for ObjC++ compatibility (#11916).

FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,27 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_
322322

323323
return [self attestKey:keyID challenge:challenge];
324324
})
325+
.recoverOn(self.queue,
326+
^id(NSError *error) {
327+
// If Apple rejected the key (DCErrorInvalidKey) then reset the attestation and
328+
// throw a specific error to signal retry (FIRAppAttestRejectionError).
329+
NSError *underlyingError = error.userInfo[NSUnderlyingErrorKey];
330+
if (underlyingError && [underlyingError.domain isEqualToString:DCErrorDomain] &&
331+
underlyingError.code == DCErrorInvalidKey) {
332+
FIRAppCheckDebugLog(
333+
kFIRLoggerAppCheckMessageCodeAttestationRejected,
334+
@"App Attest invalid key; the existing attestation will be reset.");
335+
336+
// Reset the attestation.
337+
return [self resetAttestation].thenOn(self.queue, ^NSError *(id result) {
338+
// Throw the rejection error.
339+
return [[FIRAppAttestRejectionError alloc] init];
340+
});
341+
}
342+
343+
// Otherwise just re-throw the error.
344+
return error;
345+
})
325346
.thenOn(self.queue,
326347
^FBLPromise<NSArray *> *(FIRAppAttestKeyAttestationResult *result) {
327348
// 3. Exchange the attestation to FAC token and pass the results to the next step.

FirebaseAppCheck/Tests/Unit/AppAttestProvider/FIRAppAttestProviderTests.m

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#import <XCTest/XCTest.h>
1818

19+
#import <DeviceCheck/DeviceCheck.h>
1920
#import <OCMock/OCMock.h>
2021
#import "FBLPromise+Testing.h"
2122

@@ -602,6 +603,75 @@ - (void)testGetToken_WhenAttestationIsRejected_ThenAttestationIsResetAndRetriedO
602603
[self verifyAllMocks];
603604
}
604605

606+
- (void)testGetToken_WhenExistingKeyIsRejectedByApple_ThenAttestationIsResetAndRetriedOnce_Success {
607+
// 1. Expect FIRAppAttestService.isSupported.
608+
[OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)];
609+
610+
// 2. Expect storage getAppAttestKeyID.
611+
NSString *existingKeyID = @"existingKeyID";
612+
OCMExpect([self.mockStorage getAppAttestKeyID])
613+
.andReturn([FBLPromise resolvedWith:existingKeyID]);
614+
615+
// 3. Expect a stored artifact to be requested.
616+
__auto_type rejectedPromise = [self rejectedPromiseWithError:[NSError errorWithDomain:self.name
617+
code:NSNotFound
618+
userInfo:nil]];
619+
OCMExpect([self.mockArtifactStorage getArtifactForKey:existingKeyID]).andReturn(rejectedPromise);
620+
621+
// 4. Expect random challenge to be requested.
622+
OCMExpect([self.mockAPIService getRandomChallenge])
623+
.andReturn([FBLPromise resolvedWith:self.randomChallenge]);
624+
625+
// 5. Expect the key to be attested with the challenge.
626+
NSError *attestationError = [NSError errorWithDomain:DCErrorDomain
627+
code:DCErrorInvalidKey
628+
userInfo:nil];
629+
id attestCompletionArg = [OCMArg invokeBlockWithArgs:[NSNull null], attestationError, nil];
630+
OCMExpect([self.mockAppAttestService attestKey:existingKeyID
631+
clientDataHash:self.randomChallengeHash
632+
completionHandler:attestCompletionArg]);
633+
634+
// 6. Stored attestation to be reset.
635+
[self expectAttestationReset];
636+
637+
// 7. Expect the App Attest key pair to be generated and attested.
638+
NSString *newKeyID = @"newKeyID";
639+
NSData *attestationData = [[NSUUID UUID].UUIDString dataUsingEncoding:NSUTF8StringEncoding];
640+
[self expectAppAttestKeyGeneratedAndAttestedWithKeyID:newKeyID attestationData:attestationData];
641+
642+
// 8. Expect exchange request to be sent.
643+
FIRAppCheckToken *FACToken = [[FIRAppCheckToken alloc] initWithToken:@"FAC token"
644+
expirationDate:[NSDate date]];
645+
NSData *artifactData = [@"attestation artifact" dataUsingEncoding:NSUTF8StringEncoding];
646+
__auto_type attestKeyResponse =
647+
[[FIRAppAttestAttestationResponse alloc] initWithArtifact:artifactData token:FACToken];
648+
OCMExpect([self.mockAPIService attestKeyWithAttestation:attestationData
649+
keyID:newKeyID
650+
challenge:self.randomChallenge])
651+
.andReturn([FBLPromise resolvedWith:attestKeyResponse]);
652+
653+
// 9. Expect the artifact received from Firebase backend to be saved.
654+
OCMExpect([self.mockArtifactStorage setArtifact:artifactData forKey:newKeyID])
655+
.andReturn([FBLPromise resolvedWith:artifactData]);
656+
657+
// 10. Call get token.
658+
XCTestExpectation *completionExpectation =
659+
[self expectationWithDescription:@"completionExpectation"];
660+
[self.provider
661+
getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) {
662+
[completionExpectation fulfill];
663+
664+
XCTAssertEqualObjects(token.token, FACToken.token);
665+
XCTAssertEqualObjects(token.expirationDate, FACToken.expirationDate);
666+
XCTAssertNil(error);
667+
}];
668+
669+
[self waitForExpectations:@[ completionExpectation ] timeout:0.5 enforceOrder:YES];
670+
671+
// 11. Verify mocks.
672+
[self verifyAllMocks];
673+
}
674+
605675
#pragma mark - FAC token refresh (assertion)
606676

607677
- (void)testGetToken_WhenKeyRegistered_Success {

0 commit comments

Comments
 (0)