Skip to content

Commit

Permalink
[App Check] Reset App Attest key state if attestKey fails (#11986)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewheard committed Oct 24, 2023
1 parent d5caf45 commit 1317f08
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 0 deletions.
3 changes: 3 additions & 0 deletions FirebaseAppCheck/CHANGELOG.md
@@ -1,3 +1,6 @@
# Unreleased
- [fixed] Added invalid key error handling in App Attest key attestation. (#11986)

# 10.17.0
- [fixed] Replaced semantic imports (`@import FirebaseAppCheckInterop`) with umbrella header imports
(`#import <FirebaseAppCheckInterop/FirebaseAppCheckInterop.h>`) for ObjC++ compatibility (#11916).
Expand Down
21 changes: 21 additions & 0 deletions FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m
Expand Up @@ -322,6 +322,27 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_

return [self attestKey:keyID challenge:challenge];
})
.recoverOn(self.queue,
^id(NSError *error) {
// If Apple rejected the key (DCErrorInvalidKey) then reset the attestation and
// throw a specific error to signal retry (FIRAppAttestRejectionError).
NSError *underlyingError = error.userInfo[NSUnderlyingErrorKey];
if (underlyingError && [underlyingError.domain isEqualToString:DCErrorDomain] &&
underlyingError.code == DCErrorInvalidKey) {
FIRAppCheckDebugLog(
kFIRLoggerAppCheckMessageCodeAttestationRejected,
@"App Attest invalid key; the existing attestation will be reset.");

// Reset the attestation.
return [self resetAttestation].thenOn(self.queue, ^NSError *(id result) {
// Throw the rejection error.
return [[FIRAppAttestRejectionError alloc] init];
});
}

// Otherwise just re-throw the error.
return error;
})
.thenOn(self.queue,
^FBLPromise<NSArray *> *(FIRAppAttestKeyAttestationResult *result) {
// 3. Exchange the attestation to FAC token and pass the results to the next step.
Expand Down
Expand Up @@ -16,6 +16,7 @@

#import <XCTest/XCTest.h>

#import <DeviceCheck/DeviceCheck.h>
#import <OCMock/OCMock.h>
#import "FBLPromise+Testing.h"

Expand Down Expand Up @@ -602,6 +603,75 @@ - (void)testGetToken_WhenAttestationIsRejected_ThenAttestationIsResetAndRetriedO
[self verifyAllMocks];
}

- (void)testGetToken_WhenExistingKeyIsRejectedByApple_ThenAttestationIsResetAndRetriedOnce_Success {
// 1. Expect FIRAppAttestService.isSupported.
[OCMExpect([self.mockAppAttestService isSupported]) andReturnValue:@(YES)];

// 2. Expect storage getAppAttestKeyID.
NSString *existingKeyID = @"existingKeyID";
OCMExpect([self.mockStorage getAppAttestKeyID])
.andReturn([FBLPromise resolvedWith:existingKeyID]);

// 3. Expect a stored artifact to be requested.
__auto_type rejectedPromise = [self rejectedPromiseWithError:[NSError errorWithDomain:self.name
code:NSNotFound
userInfo:nil]];
OCMExpect([self.mockArtifactStorage getArtifactForKey:existingKeyID]).andReturn(rejectedPromise);

// 4. Expect random challenge to be requested.
OCMExpect([self.mockAPIService getRandomChallenge])
.andReturn([FBLPromise resolvedWith:self.randomChallenge]);

// 5. Expect the key to be attested with the challenge.
NSError *attestationError = [NSError errorWithDomain:DCErrorDomain
code:DCErrorInvalidKey
userInfo:nil];
id attestCompletionArg = [OCMArg invokeBlockWithArgs:[NSNull null], attestationError, nil];
OCMExpect([self.mockAppAttestService attestKey:existingKeyID
clientDataHash:self.randomChallengeHash
completionHandler:attestCompletionArg]);

// 6. Stored attestation to be reset.
[self expectAttestationReset];

// 7. Expect the App Attest key pair to be generated and attested.
NSString *newKeyID = @"newKeyID";
NSData *attestationData = [[NSUUID UUID].UUIDString dataUsingEncoding:NSUTF8StringEncoding];
[self expectAppAttestKeyGeneratedAndAttestedWithKeyID:newKeyID attestationData:attestationData];

// 8. Expect exchange request to be sent.
FIRAppCheckToken *FACToken = [[FIRAppCheckToken alloc] initWithToken:@"FAC token"
expirationDate:[NSDate date]];
NSData *artifactData = [@"attestation artifact" dataUsingEncoding:NSUTF8StringEncoding];
__auto_type attestKeyResponse =
[[FIRAppAttestAttestationResponse alloc] initWithArtifact:artifactData token:FACToken];
OCMExpect([self.mockAPIService attestKeyWithAttestation:attestationData
keyID:newKeyID
challenge:self.randomChallenge])
.andReturn([FBLPromise resolvedWith:attestKeyResponse]);

// 9. Expect the artifact received from Firebase backend to be saved.
OCMExpect([self.mockArtifactStorage setArtifact:artifactData forKey:newKeyID])
.andReturn([FBLPromise resolvedWith:artifactData]);

// 10. Call get token.
XCTestExpectation *completionExpectation =
[self expectationWithDescription:@"completionExpectation"];
[self.provider
getTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) {
[completionExpectation fulfill];

XCTAssertEqualObjects(token.token, FACToken.token);
XCTAssertEqualObjects(token.expirationDate, FACToken.expirationDate);
XCTAssertNil(error);
}];

[self waitForExpectations:@[ completionExpectation ] timeout:0.5 enforceOrder:YES];

// 11. Verify mocks.
[self verifyAllMocks];
}

#pragma mark - FAC token refresh (assertion)

- (void)testGetToken_WhenKeyRegistered_Success {
Expand Down

0 comments on commit 1317f08

Please sign in to comment.