Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions Sources/AppStoreServerLibrary/AppStoreServerAPIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import NIOFoundationCompat

public class AppStoreServerAPIClient {

public enum ConfigurationError: Error {
public enum ConfigurationError: Error, Hashable, Sendable {
/// Xcode is not a supported environment for an AppStoreServerAPIClient
case invalidEnvironment
}
Expand Down Expand Up @@ -345,7 +345,9 @@ public enum APIResult<T> {
case failure(statusCode: Int?, rawApiError: Int64?, apiError: APIError?, errorMessage: String?, causedBy: Error?)
}

public enum APIError: Int64 {
extension APIResult: Sendable where T: Sendable {}

public enum APIError: Int64, Hashable, Sendable {
///An error that indicates an invalid request.
///
///[GeneralBadRequestError](https://developer.apple.com/documentation/appstoreserverapi/generalbadrequesterror)
Expand Down Expand Up @@ -633,7 +635,7 @@ public enum APIError: Int64 {
case generalInternalRetryable = 5000001
}

public enum GetTransactionHistoryVersion: String {
public enum GetTransactionHistoryVersion: String, Hashable, Sendable {
@available(*, deprecated)
case v1 = "v1"
case v2 = "v2"
Expand Down
13 changes: 8 additions & 5 deletions Sources/AppStoreServerLibrary/ChainVerifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import Crypto
import AsyncHTTPClient
import NIOFoundationCompat

class ChainVerifier {

actor ChainVerifier {
private static let EXPECTED_CHAIN_LENGTH = 3
private static let EXPECTED_JWT_SEGMENTS = 3
private static let EXPECTED_ALGORITHM = "ES256"
Expand All @@ -28,7 +27,7 @@ class ChainVerifier {
self.verifiedPublicKeyCache = [:]
}

func verify<T: DecodedSignedData>(signedData: String, type: T.Type, onlineVerification: Bool, environment: AppStoreEnvironment) async -> VerificationResult<T> where T: Decodable {
func verify<T: DecodedSignedData>(signedData: String, type: T.Type, onlineVerification: Bool, environment: AppStoreEnvironment) async -> VerificationResult<T> where T: Decodable & Sendable {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since ChainVerifier is now an actor, T must be Sendable here.

let header: JWTHeader;
let decodedBody: T;
do {
Expand Down Expand Up @@ -120,7 +119,7 @@ class ChainVerifier {
return verificationResult
}

func verifyChainWithoutCaching(leaf: Certificate, intermediate: Certificate, online: Bool, validationTime: Date) async -> X509.VerificationResult {
nonisolated func verifyChainWithoutCaching(leaf: Certificate, intermediate: Certificate, online: Bool, validationTime: Date) async -> X509.VerificationResult {
var verifier = Verifier(rootCertificates: self.store) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since Verifier is not Sendable, but becomes tied to the actor, we must mark this method nonisolated to detach this dependency and allow it to be safely used within the context of this method.

RFC5280Policy(validationTime: validationTime)
AppStoreOIDPolicy()
Expand All @@ -132,7 +131,7 @@ class ChainVerifier {
return await verifier.validate(leafCertificate: leaf, intermediates: intermediateStore)
}

func getDate() -> Date {
nonisolated func getDate() -> Date {
return Date()
}
Comment on lines -135 to 136
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what the intent on making a separate getDate() method was, but it must now be nonisolated so it can be called by the above method, which is also nonisolated.

}
Expand Down Expand Up @@ -226,6 +225,10 @@ public enum VerificationResult<T> {
case invalid(VerificationError)
}

extension VerificationResult: Equatable where T: Equatable {}
extension VerificationResult: Hashable where T: Hashable {}
extension VerificationResult: Sendable where T: Sendable {}

public enum VerificationError: Hashable, Sendable {
case INVALID_JWT_FORMAT
case INVALID_CERTIFICATE
Expand Down
6 changes: 3 additions & 3 deletions Sources/AppStoreServerLibrary/SignedDataVerifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import Foundation

///A verifier and decoder class designed to decode signed data from the App Store.
public struct SignedDataVerifier {
public struct SignedDataVerifier: Sendable {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SignedDataVerifier must be sendable in order for it to be used by multiple concurrent tasks at the same time. This means that ChainVerifier must be updated to an actor to properly prevent data race issues.


public enum ConfigurationError: Error {
public enum ConfigurationError: Error, Hashable, Sendable {
case INVALID_APP_APPLE_ID
}

Expand Down Expand Up @@ -148,7 +148,7 @@ public struct SignedDataVerifier {
return appTransactionResult
}

private func decodeSignedData<T: DecodedSignedData>(signedData: String, type: T.Type) async -> VerificationResult<T> where T : Decodable {
private func decodeSignedData<T: DecodedSignedData>(signedData: String, type: T.Type) async -> VerificationResult<T> where T : Decodable & Sendable {
return await chainVerifier.verify(signedData: signedData, type: type, onlineVerification: self.enableOnlineChecks, environment: self.environment)
}
}