// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import Foundation private let kGenerateAppAttestChallengeEndpoint = "generateAppAttestChallenge" private let kExchangeAppAttestAttestationEndpoint = "exchangeAppAttestAttestation" private let kExchangeAppAttestAssertionEndpoint = "exchangeAppAttestAssertion" private let kRequestFieldArtifact = "artifact" private let kRequestFieldAssertion = "assertion" private let kRequestFieldChallenge = "challenge" private let kRequestFieldKeyID = "key_id" private let kRequestFieldAttestation = "attestation_statement" private let kRequestFieldLimitedUse = "limited_use" private let kContentTypeKey = "Content-Type" private let kJSONContentType = "application/json" private let kHTTPMethodPost = "POST" protocol AppCheckCoreAppAttestAPIServiceProtocol: NSObjectProtocol { func getRandomChallenge() async throws -> Data func attestKey(withAttestation attestation: Data, keyID: String, challenge: Data, limitedUse: Bool) async throws -> AppCheckCoreAppAttestAttestationResponse func getAppCheckToken(withArtifact artifact: Data, challenge: Data, assertion: Data, limitedUse: Bool) async throws -> AppCheckCoreToken } class AppCheckCoreAppAttestAPIService: NSObject, AppCheckCoreAppAttestAPIServiceProtocol { private let apiService: AppCheckCoreAPIServiceProtocol private let resourceName: String init(apiService: AppCheckCoreAPIServiceProtocol, resourceName: String) { self.apiService = apiService self.resourceName = resourceName super.init() } // MARK: - API Calls func getRandomChallenge() async throws -> Data { let url = try urlForEndpoint(kGenerateAppAttestChallengeEndpoint) let response = try await apiService.sendRequest( withURL: url, httpMethod: kHTTPMethodPost, body: nil, additionalHeaders: nil ) return try randomChallengeWithAPIResponse(response) } func attestKey(withAttestation attestation: Data, keyID: String, challenge: Data, limitedUse: Bool) async throws -> AppCheckCoreAppAttestAttestationResponse { let url = try urlForEndpoint(kExchangeAppAttestAttestationEndpoint) let body = try httpBody( withAttestation: attestation, keyID: keyID, challenge: challenge, limitedUse: limitedUse ) let urlResponse = try await apiService.sendRequest( withURL: url, httpMethod: kHTTPMethodPost, body: body, additionalHeaders: [kContentTypeKey: kJSONContentType] ) guard let responseData = urlResponse.httpBody else { throw AppCheckCoreErrorUtil.error(withFailureReason: "Invalid or missing response data.") } let response = try AppCheckCoreAppAttestAttestationResponse( responseData: responseData, requestDate: urlResponse.requestDate ) return response } func getAppCheckToken(withArtifact artifact: Data, challenge: Data, assertion: Data, limitedUse: Bool) async throws -> AppCheckCoreToken { let url = try urlForEndpoint(kExchangeAppAttestAssertionEndpoint) let body = try httpBody( withArtifact: artifact, challenge: challenge, assertion: assertion, limitedUse: limitedUse ) let urlResponse = try await apiService.sendRequest( withURL: url, httpMethod: kHTTPMethodPost, body: body, additionalHeaders: [kContentTypeKey: kJSONContentType] ) let token = try apiService.appCheckToken(withAPIResponse: urlResponse) return token } // MARK: - Challenge parsing private func randomChallengeWithAPIResponse(_ response: AppCheckCoreURLSessionDataResponse) throws -> Data { guard let responseData = response.httpBody else { throw AppCheckCoreErrorUtil.error(withFailureReason: "Empty server response body.") } if responseData.isEmpty { throw AppCheckCoreErrorUtil.error(withFailureReason: "Empty server response body.") } guard let responseDict = try? JSONSerialization .jsonObject(with: responseData, options: []) as? [String: Any] else { throw AppCheckCoreErrorUtil.jsonSerializationError(NSError( domain: NSCocoaErrorDomain, code: 0, userInfo: nil )) } guard let challengeBase64 = responseDict["challenge"] as? String else { throw AppCheckCoreErrorUtil.appCheckTokenResponseError(withMissingField: "challenge") } guard let challenge = Data(base64Encoded: challengeBase64) else { throw AppCheckCoreErrorUtil.error(withFailureReason: "Invalid base64 string for challenge.") } return challenge } // MARK: - Body Builders private func httpBody(withAttestation attestation: Data, keyID: String, challenge: Data, limitedUse: Bool) throws -> Data { if attestation.isEmpty || keyID.isEmpty || challenge.isEmpty { throw AppCheckCoreErrorUtil.error(withFailureReason: "Missing or empty request parameter.") } let jsonObject: [String: Any] = [ kRequestFieldKeyID: keyID, kRequestFieldAttestation: attestation.base64EncodedString(), kRequestFieldChallenge: challenge.base64EncodedString(), kRequestFieldLimitedUse: limitedUse, ] return try httpBody(withJSONObject: jsonObject) } private func httpBody(withArtifact artifact: Data, challenge: Data, assertion: Data, limitedUse: Bool) throws -> Data { if artifact.isEmpty || challenge.isEmpty || assertion.isEmpty { throw AppCheckCoreErrorUtil.error(withFailureReason: "Missing or empty request parameter.") } let jsonObject: [String: Any] = [ kRequestFieldArtifact: artifact.base64EncodedString(), kRequestFieldChallenge: challenge.base64EncodedString(), kRequestFieldAssertion: assertion.base64EncodedString(), kRequestFieldLimitedUse: limitedUse, ] return try httpBody(withJSONObject: jsonObject) } private func httpBody(withJSONObject jsonObject: Any) throws -> Data { do { return try JSONSerialization.data(withJSONObject: jsonObject, options: []) } catch { throw AppCheckCoreErrorUtil.jsonSerializationError(error as NSError) } } // MARK: - URL Helpers private func urlForEndpoint(_ endpoint: String) throws -> URL { let urlString = "\(apiService.baseURL)/\(resourceName):\(endpoint)" guard let url = URL(string: urlString) else { throw AppCheckCoreErrorUtil.error(withFailureReason: "Invalid URL.") } return url } }