Files
app-check/AppCheckCore/Sources/AppAttestProvider/API/AppCheckCoreAppAttestAPIService.swift
2026-09-23 18:22:08 -04:00

197 lines
6.9 KiB
Swift

// 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
}
}