diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000..585df56 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,61 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + macos: + name: macOS (Build & Test) + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Build + run: swift build -v + - name: Test + run: swift test -v + + ios: + name: iOS (Build & Test) + runs-on: macos-26 + steps: + - uses: actions/checkout@v4 + - name: List available simulators + run: | + xcrun simctl list devices + xcodebuild -showsdks + - name: Build + run: xcodebuild build -scheme TPInAppReceipt -destination 'platform=iOS Simulator,OS=latest,name=iPhone 17' -skipPackagePluginValidation SWIFT_STRICT_CONCURRENCY=targeted + - name: Test + run: xcodebuild test -scheme TPInAppReceipt -destination 'platform=iOS Simulator,OS=latest,name=iPhone 17' -skipPackagePluginValidation SWIFT_STRICT_CONCURRENCY=targeted + + linux: + name: Linux (Build & Test) + runs-on: ubuntu-latest + container: swift:6.0 + steps: + - uses: actions/checkout@v4 + + - name: Show Swift & OS info + run: | + swift --version + uname -a + + - name: Restore swift package cache + uses: actions/cache@v4 + with: + path: | + .build + .swiftpm + key: ${{ runner.os }}-swift-${{ hashFiles('**/Package.resolved') }} + + - name: Resolve Packages + run: swift package resolve -v + + - name: Build + run: swift build -v + + - name: Test + run: swift test -v diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml deleted file mode 100644 index 59e3574..0000000 --- a/.github/workflows/swift.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Swift - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - build: - - runs-on: macos-latest - - steps: - - uses: actions/checkout@v2 - - name: Build - run: swift build -v - diff --git a/.gitignore b/.gitignore index 1328f51..0077b67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,74 +1,13 @@ -# Xcode -# -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore - -## Build generated -build/ -DerivedData/ -Packages/ -iOS Example/ViewController.swift - -## Various settings -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 +.DS_Store +/.build +/Packages +/*.xcodeproj xcuserdata/ - -## Other -*.moved-aside -*.xcuserstate - -## Obj-C/Swift specific -*.hmap -*.ipa -*.dSYM.zip -*.dSYM - -## Playgrounds -timeline.xctimeline -playground.xcworkspace - -# Swift Package Manager -# -# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. -# Packages/ -.build/ - -# CocoaPods -# -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control -# -# Pods/ - -# Carthage -# -# Add this line if you want to avoid checking in source code from Carthage dependencies. -# Carthage/Checkouts - -Carthage/Build - -# fastlane -# -# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the -# screenshots whenever they are needed. -# For more information about the recommended setup visit: -# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md - -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots -fastlane/test_output - -# AppCode -.idea - -TPInAppReceiptTests/MyDevice.swift - -.swiftpm/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +Package.resolved +.swiftpm +/out +.claude diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 0000000..c9d8ed1 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,4 @@ +version: 1 +builder: + configs: + - documentation_targets: [TPInAppReceipt] diff --git a/.swift-format b/.swift-format new file mode 100644 index 0000000..c906083 --- /dev/null +++ b/.swift-format @@ -0,0 +1,58 @@ +{ + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "private" + }, + "indentation" : { + "spaces" : 4 + }, + "indentConditionalCompilationBlocks" : false, + "indentSwitchCaseLabels" : false, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : true, + "lineBreakBeforeEachGenericRequirement" : true, + "lineLength" : 120, + "maximumBlankLines" : 1, + "prioritizeKeepingFunctionOutputTogether" : true, + "respectsExistingLineBreaks" : true, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : false, + "AlwaysUseLowerCamelCase" : false, + "AmbiguousTrailingClosureOverload" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : true, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : false, + "NeverUseForceTry" : false, + "NeverUseImplicitlyUnwrappedOptionals" : false, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoAssignmentInExpressions" : true, + "NoBlockComments" : true, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : false, + "NoLeadingUnderscores" : false, + "NoParensAroundConditions" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : true, + "OrderedImports" : false, + "ReturnVoidInsteadOfEmptyTuple" : true, + "UseEarlyExits" : true, + "UseLetInEveryBoundCaseVariable" : false, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : false, + "UseSynthesizedInitializer" : false, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : false, + "ValidateDocumentationComments" : false + }, + "spacesAroundRangeFormationOperators" : false, + "tabWidth" : 4, + "version" : 1 +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c8f0fe8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog + +## 4.0.0 + +### Breaking Changes + +- `InAppReceipt` class replaced by `AppReceipt` typealias over `ContentInfo>` +- Sync validation (`try receipt.verify()`) replaced by async (`await receipt.validate()`) +- Error types renamed: `IARError` → `AppReceiptError`, `ReceiptValidatorError` +- `originalAppVersion` is now `String?` (was `String`) +- `ageRating` is now `String?` (was `String`) +- Minimum platforms raised: iOS 13+, macOS 10.15+, tvOS 13+ +- Swift tools version: 6.0 +- Removed Objective-C support +- Removed CocoaPods support + +### New + +- Full PKCS#7 structure: `ContentInfo`, `SignedData`, `EncapsulatedContentInfo`, `SignerInfo`, `AlgorithmIdentifier`, etc. +- Composable validation via `@VerifierBuilder` and `ReceiptValidator` +- X.509 certificate chain verification using swift-certificates +- Signature verification using swift-crypto +- New receipt fields: `environment`, `appStoreID`, `transactionDate`, `fulfillmentToolVersion`, `developerID`, `downloadID`, `installerVersionID` +- `InAppReceiptPayload.Environment` enum (`.production`, `.sandbox`, `.xcode`, `.productionSandbox`) +- `InAppPurchase.Type` enum (`.consumable`, `.nonConsumable`, `.autoRenewableSubscription`, `.nonRenewingSubscription`) +- Blocking API via `@_spi(Blocking)`: `local_blocking`, `validate_blocking()` +- Pluggable decoder via `AppReceiptDecoder.Engine` protocol +- DocC documentation +- Privacy manifest (`PrivacyInfo.xcprivacy`) +- visionOS support + +### Dependencies + +- Removed: ASN1Swift +- Added: [swift-asn1](https://github.com/apple/swift-asn1) 1.5.0+, [swift-certificates](https://github.com/apple/swift-certificates) 1.15.1+, [swift-crypto](https://github.com/apple/swift-crypto) 4.2.0+ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 20746e1..b839e0a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,13 +1,54 @@ # Contributing to TPInAppReceipt -### All contributions to TPInAppReceipt are welcome. +Thank you for your interest in contributing to TPInAppReceipt! This document provides guidelines and instructions for contributing. -If you want to help developing TPInAppReceipt, please take a look for any opened issue or you can introduce a new functionality. +## Reporting Bugs -### Increase unit test coverage +If you find a bug, please open an issue on [GitHub Issues](https://github.com/tikhop/TPInAppReceipt/issues) with: -Additional unit test coverage is welcome. +- A clear, descriptive title +- Steps to reproduce the issue +- Expected behavior vs actual behavior +- Swift version and platform information +- Minimal code example if possible -## Issues +## Suggesting Features -If TPInAppReceipt doesn't work as you expect, please review [any open issues](https://github.com/tikhop/TPInAppReceipt/issues) before opening a new one. +Feature requests are welcome! Please open an issue with: + +- A clear description of the feature +- Use cases and motivation +- Any implementation ideas you may have + +## Pull Request Process + +1. Fork the repository and create your branch from `master` +2. Make your changes and ensure tests pass +3. Add tests for any new functionality +4. Update documentation if needed +5. Submit a pull request with a clear description of your changes + +### PR Guidelines + +- Keep changes focused and atomic +- Follow existing code patterns +- Ensure all tests pass before submitting + +## Code Style + +This project follows Swift conventions. Please reference `.swift-format` for formatting guidelines. + +To format all files: `swift format -i . --recursive` + +Key points: +- Keep lines reasonably short +- Use meaningful variable and function names + +## Testing Requirements + +All contributions should include appropriate tests: + +- Run existing tests with `swift test` +- Add unit tests for new functionality + +Thank you for contributing! diff --git a/Documentation/UseInObjCProject.md b/Documentation/UseInObjCProject.md deleted file mode 100644 index 2584132..0000000 --- a/Documentation/UseInObjCProject.md +++ /dev/null @@ -1,44 +0,0 @@ -# Use TPInAppReceipt in Objective-C project - -Installation ------------- - -### CocoaPods - -To integrate TPInAppReceipt into your Objective-C project using CocoaPods, specify it in your `Podfile`: - -```ruby -platform :ios, '9.0' - -target 'YOUR_TARGET' do -use_frameworks! - -pod 'TPInAppReceipt/Objc' -end - -``` - -Then, run the following command: - -```bash -$ pod install -``` - -In any swift file you'd like to use TPInAppReceipt, import the framework with `@import TPInAppReceipt;`. - -### Swift Package Manager - -To integrate using Apple's Swift package manager, add the following as a dependency to your `Package.swift`: - -```swift -.package(url: "https://github.com/tikhop/TPInAppReceipt.git", .upToNextMajor(from: "3.0.0")) -``` - -Then, specify `"TPInAppReceipt-Objc"` as a dependency of the Target in which you wish to use TPInAppReceipt. - -Lastly, run the following command: -```swift -swift package update -``` - -Once you are done with SPM, you can import the framework with `@import TPInAppReceipt_Objc;`. diff --git a/LICENSE b/LICENSE index c111a51..2b3ff8a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016-present Pavel Tikhoneko +Copyright (c) 2016-Present Pavel Tikhonenko Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index c26287d..0000000 --- a/Package.resolved +++ /dev/null @@ -1,14 +0,0 @@ -{ - "pins" : [ - { - "identity" : "asn1swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tikhop/ASN1Swift", - "state" : { - "revision" : "4e525ca3064fd45751b80c9ae6a2c16ea7b395e9", - "version" : "1.2.6" - } - } - ], - "version" : 2 -} diff --git a/Package.swift b/Package.swift index fc0e116..e99ce43 100644 --- a/Package.swift +++ b/Package.swift @@ -1,43 +1,72 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.0 import PackageDescription +#if canImport(Darwin) +let privacyManifestExclude: [String] = [] +let privacyManifestResource: [PackageDescription.Resource] = [.copy("Resources/PrivacyInfo.xcprivacy")] +#else +// Exclude on other platforms to avoid build warnings. +let privacyManifestExclude: [String] = ["Resources/PrivacyInfo.xcprivacy"] +let privacyManifestResource: [PackageDescription.Resource] = [] +#endif + +#if canImport(Darwin) +let httpClientDependency: [Package.Dependency] = [] +let httpClientTargetDependency: [Target.Dependency] = [] +#else +let httpClientDependency: [Package.Dependency] = [ + .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.24.0") +] +let httpClientTargetDependency: [Target.Dependency] = [ + .product(name: "AsyncHTTPClient", package: "async-http-client") +] +#endif + let package = Package( name: "TPInAppReceipt", - platforms: [.macOS(.v10_13), - .iOS(.v12), - .tvOS(.v12), - .watchOS("6.2"), - .visionOS(.v1), - .macCatalyst(.v13)], - + platforms: [ + .macOS(.v10_15), + .iOS(.v13), + .tvOS(.v13), + .watchOS("6.2"), + .visionOS(.v1), + .macCatalyst(.v13), + ], + products: [ - .library(name: "TPInAppReceipt", targets: ["TPInAppReceipt"]), - .library(name: "TPInAppReceipt-Objc", targets: ["TPInAppReceipt-Objc"]), + .library(name: "TPInAppReceipt", targets: ["TPInAppReceipt"]) ], - dependencies: [ - .package(url: "https://github.com/tikhop/ASN1Swift", .upToNextMajor(from: "1.0.0")) - ], + dependencies: [ + .package(url: "https://github.com/apple/swift-asn1", .upToNextMajor(from: "1.5.0")), + .package(url: "https://github.com/apple/swift-certificates.git", .upToNextMajor(from: "1.15.1")), + .package(url: "https://github.com/apple/swift-crypto.git", .upToNextMajor(from: "4.2.0")), + ] + httpClientDependency, targets: [ .target( name: "TPInAppReceipt", - dependencies: ["ASN1Swift"], - path: "Sources", - exclude: ["Bundle+Private.swift", "Objc/InAppReceipt+Objc.swift"], - resources: [.process("AppleIncRootCertificate.cer"), - .process("StoreKitTestCertificate.cer"), - .process("PrivacyInfo.xcprivacy")] - ), - .target( - name: "TPInAppReceipt-Objc", - dependencies: ["TPInAppReceipt"], - path: "Sources/Objc" - ), - .testTarget( - name: "TPInAppReceiptTests", - dependencies: ["TPInAppReceipt"]) - ], - swiftLanguageVersions: [.v5] + dependencies: [ + .product(name: "SwiftASN1", package: "swift-asn1"), + .product(name: "X509", package: "swift-certificates"), + .product(name: "Crypto", package: "swift-crypto"), + ] + httpClientTargetDependency, + path: "Sources", + exclude: privacyManifestExclude, + resources: privacyManifestResource + [ + .process("Resources/AppleIncRootCertificate.cer"), + .process("Resources/StoreKitTestCertificate.cer"), + ] + ), + .testTarget( + name: "TPInAppReceiptTests", + dependencies: [ + "TPInAppReceipt", + .product(name: "SwiftASN1", package: "swift-asn1"), + .product(name: "X509", package: "swift-certificates"), + .product(name: "Crypto", package: "swift-crypto"), + ], + path: "Tests", + resources: [.copy("Assets")] + ), + ] ) - - diff --git a/README.md b/README.md index 75d4969..140763b 100644 --- a/README.md +++ b/README.md @@ -2,201 +2,96 @@

- # TPInAppReceipt ![Swift](https://github.com/tikhop/TPInAppReceipt/workflows/Swift/badge.svg?branch=master) -[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/TPInAppReceipt.svg)](https://cocoapods.org/pods/TPInAppReceipt) [![Swift Package Manager compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) -[![Platform](https://img.shields.io/cocoapods/p/TPInAppReceipt.svg?style=flat)]() [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/tikhop/TPInAppReceipt/master/LICENSE) -TPInAppReceipt is a lightweight, pure-Swift library for reading and validating Apple In App Purchase Receipt locally. - -## Features - -- [x] Read all In-App Receipt Attributes -- [x] Validate In-App Purchase Receipt (Signature, Bundle Version and Identifier, Hash) -- [x] Determine Eligibility for Introductory Offer -- [x] Use with StoreKitTest -- [x] Use in Objective-C projects - -Installation ------------- - -> Note: [TPInAppReceipt in Objective-C project](https://github.com/tikhop/TPInAppReceipt/blob/master/Documentation/UseInObjCProject.md) - If you want to use TPInAppReceipt in Objective-C project please follow this guide. - -### CocoaPods +Local decoding and validation of Apple App Store receipts. Built on top of Apple's [swift-asn1](https://github.com/apple/swift-asn1), [swift-certificates](https://github.com/apple/swift-certificates), and [swift-crypto](https://github.com/apple/swift-crypto). -To integrate TPInAppReceipt into your project using CocoaPods, specify it in your `Podfile`: - -```ruby -platform :ios, '12.0' - -target 'YOUR_TARGET' do - use_frameworks! +```swift +let receipt = try await AppReceipt.local - pod 'TPInAppReceipt' -end +receipt.bundleIdentifier // "com.example.app" +receipt.environment // .production, .sandbox, .xcode +receipt.purchases // [InAppPurchase] +let result = await receipt.validate() ``` -Then, run the following command: +## Requirements -```bash -$ pod install -``` +- Swift 6.0+ / Xcode 16+ +- macOS 10.15+ / iOS 13+ / tvOS 13+ / watchOS 6.2+ / visionOS 1+ -In any swift file you'd like to use TPInAppReceipt, import the framework with `import TPInAppReceipt`. +## Installation ### Swift Package Manager -To integrate using Apple's Swift package manager, add the following as a dependency to your `Package.swift`: - ```swift -.package(url: "https://github.com/tikhop/TPInAppReceipt.git", .upToNextMajor(from: "3.0.0")) +dependencies: [ + .package(url: "https://github.com/tikhop/TPInAppReceipt.git", from: "4.0.0") +] ``` -Then, specify `"TPInAppReceipt"` as a dependency of the Target in which you wish to use TPInAppReceipt. - -Lastly, run the following command: ```swift -swift package update +.target( + name: "YourTarget", + dependencies: ["TPInAppReceipt"] +) ``` -### Requirements +## Usage -- iOS 12.0+ / OSX 10.13+ -- Swift 5.9+ - -Usage -------------- - -### Working With a Receipt - -The [`InAppReceipt`](https://tikhop.github.io/TPInAppReceipt/Classes/InAppReceipt.html) object encapsulates information about a receipt and the purchases associated with it. To validate In-App Purchase Receipt you must create an `InAppReceipt` object. - -#### Initializing Receipt - -To create [`InAppReceipt`](https://tikhop.github.io/TPInAppReceipt/Classes/InAppReceipt.html) object you can either provide a raw receipt data or initialize a local receipt. +### Decoding and Reading ```swift -do { - /// Initialize receipt - let receipt = try InAppReceipt.localReceipt() - // let receipt = try InAppReceipt() // Returns local receipt - - // let receiptData: Data = ... - // let receipt = try InAppReceipt.receipt(from: receiptData) - -} catch { - print(error) -} +import TPInAppReceipt +// Local receipt +let receipt = try await AppReceipt.local +// From raw data +let receipt = try AppReceipt.receipt(from: data) ``` -#### Validating Receipt +Access receipt fields, query purchases, check subscriptions and introductory offer eligibility. See for details. -`TPInAppReceipt` provides a variety of convenience methods for validating In-App Purchase Receipt: +### Validating + +Default validation: certificate chain + signature + hash + metadata. ```swift +let result = await receipt.validate() -/// Verify hash -try? receipt.verifyHash() - -/// Verify bundle identifier -try? receipt.verifyBundleIdentifier() - -/// Verify bundle version -try? receipt.verifyBundleVersion() - -/// Verify signature -try? receipt.verifySignature() - -/// Validate all at once -do { - try receipt.verify() -} catch IARError.validationFailed(reason: .hashValidation) { - // Do smth -} catch IARError.validationFailed(reason: .bundleIdentifierVerification) { - // Do smth -} catch IARError.validationFailed(reason: .signatureValidation) { - // Do smth -} catch { - // Do smth +switch result { +case .valid: + break +case .invalid(let error): + print(error) } - ``` -> NOTE: Apple recommends to perform receipt validation right after your app is launched. For additional security, you may repeat this check periodically while your application is running. -> NOTE: If validation fails in iOS, try to refresh the receipt first. +Supports custom validators via `@VerifierBuilder`. See for details. -#### Determining Eligibility for Introductory Offer +### Blocking API -If your App offers introductory pricing for auto-renewable subscriptions, you will need to dispay the correct price, either the intro or regular price. -The [`InAppReceipt`](https://tikhop.github.io/TPInAppReceipt/Classes/InAppReceipt.html) class provides an interface for determining introductory price eligibility. At the simplest, just provide a `Set` of product identifiers that belong to the same subscription group: +For contexts where async is not available. See . ```swift -// Check whether user is eligible for any products within the same subscription group -var isEligible = receipt.isEligibleForIntroductoryOffer(for: ["com.test.product.bronze", "com.test.product.silver", "com.test.product.gold"]) -``` - -> Note: To determine if a user is eligible for an introductory offer, you must initialize and validate receipt first and only then check for eligibility. - - -#### Reading Receipt - -```swift -/// Initialize receipt -let receipt = try! InAppReceipt.localReceipt() - -/// Base64 Encoded Receipt -let base64Receipt = receipt.base64 - -/// Check whether receipt contains any purchases -let hasPurchases = receipt.hasPurchases - -/// All auto renewable `InAppPurchase`s, -let purchases: [InAppPurchase] = receipt.autoRenewablePurchases - -/// all ACTIVE auto renewable `InAppPurchase`s, -let activePurchases: [InAppPurchase] = receipt.activeAutoRenewableSubscriptionPurchases - -``` - -#### Useful methods - -```swift - -// Retrieve Original TransactionIdentifier for Product Name -receipt.originalTransactionIdentifier(ofProductIdentifier: subscriptionName) - -// Retrieve Active Auto Renewable Subscription's Purchases for Product Name and Specific Date -receipt.activeAutoRenewableSubscriptionPurchases(ofProductIdentifier: subscriptionName, forDate: Date()) - -// Retrieve All Purchases for Product Name -receipt.purchases(ofProductIdentifier: subscriptionName) +@_spi(Blocking) import TPInAppReceipt +let receipt = try AppReceipt.local_blocking +let result = receipt.validate_blocking() ``` -#### Refreshing/Requesting Receipt +## Migrating from v3 -When necessary, use this method to ensure the receipt you are working with is up-to-date. - -```swift -InAppReceipt.refresh { (error) in - if let err = error - { - print(err) - } else { - initializeReceipt() - } -} - -``` +See . ## Essential Reading + * [Apple - About Receipt Validation](https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Introduction.html) * [Apple - Receipt Validation Programming Guide](https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1) * [Apple - Validating Receipts Locally](https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html) @@ -204,8 +99,6 @@ InAppReceipt.refresh { (error) in * [Faisal Bin Ahmed - All the wrong ways to persist in-app purchase status in your macOS app](https://medium.com/@Faisalbin/all-the-wrong-ways-to-persist-in-app-purchase-status-in-your-macos-app-ce6eb9bcb0c3) * [objc.io - Receipt Validation](https://www.objc.io/issues/17-security/receipt-validation/) - - ## License -TPInAppReceipt is released under an MIT license. See [LICENSE](https://github.com/tikhop/TPInAppReceipt/blob/master/LICENSE) for more information. +MIT diff --git a/Sources/AppReceipt.swift b/Sources/AppReceipt.swift new file mode 100644 index 0000000..46d4c1c --- /dev/null +++ b/Sources/AppReceipt.swift @@ -0,0 +1,144 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +public typealias AppReceipt = ContentInfo> + +extension AppReceipt { + /// The environment in which the receipt was generated. + public var environment: InAppReceiptPayload.Environment { + payload.environment + } + + /// The app's bundle identifier. + /// + /// Uniquely identifies the application that generated the receipt. + public var bundleIdentifier: String { + payload.bundleIdentifier + } + + /// The app's version number. + /// + /// The current version of the app at the time the receipt was generated. + public var appVersion: String { + payload.appVersion + } + + /// The version of the app that was originally purchased. + /// + /// This represents the version of the app when the user first purchased it. + public var originalAppVersion: String? { + payload.originalAppVersion + } + + /// The date of the app that was originally purchased. + /// + /// The date when the user first purchased the app. + public var originalPurchaseDate: Date? { + payload.originalPurchaseDate + } + + /// All in-app purchases in this receipt. + /// + /// Returns an array of ``InAppPurchase`` objects representing all purchases included in the receipt. + public var purchases: [InAppPurchase] { + payload.purchases + } + + /// All auto-renewable subscription purchases in this receipt. + /// + /// Returns an array of ``InAppPurchase`` objects that are auto-renewable subscriptions. + public var autoRenewablePurchases: [InAppPurchase] { + purchases.filter { $0.isRenewableSubscription } + } + + /// All currently active auto-renewable subscription purchases in this receipt. + /// + /// Returns an array of ``InAppPurchase`` objects that are auto-renewable subscriptions + /// and are currently active as of the current date. + public var activeAutoRenewableSubscriptionPurchases: [InAppPurchase] { + purchases + .filter { + $0.isRenewableSubscription && $0.isActiveAutoRenewableSubscription(forDate: Date()) + } + } + + /// The date that the app receipt expires. + /// + /// For most apps, this is the expiration date of the last auto-renewable subscription. + public var expirationDate: Date? { + payload.expirationDate + } + + /// Indicates whether the receipt contains any purchases. + /// + /// Returns `true` if any purchases exist, `false` otherwise. + public var hasPurchases: Bool { + purchases.count > 0 + } + + /// Indicates whether the receipt contains any active auto-renewable purchases. + /// + /// Returns `true` if any active auto-renewable subscription purchases exist, `false` otherwise. + public var hasActiveAutoRenewablePurchases: Bool { + activeAutoRenewableSubscriptionPurchases.count > 0 + } + + /// The date the receipt was created. + /// + /// The date and time when the receipt was generated by Apple. + public var creationDate: Date { + payload.creationDate + } + + /// The app's age rating. + /// + /// A string value representing the age rating for the app. + public var ageRating: String? { + payload.ageRating + } +} + +extension AppReceipt { + /// The decoded receipt payload. + /// + /// Extracts and returns the in-app receipt payload data from the receipt structure. + public var payload: InAppReceiptPayload { + guard let payload = content.encapContentInfo.eContent else { + fatalError("Invalid receipt payload") + } + + return payload + } + + /// The raw payload data. + /// + /// Returns the raw bytes of the receipt payload in its original encoded format. + public var payloadRawData: Data { + guard + let payload = content.encapContentInfo.eContentRaw, + !payload.bytes.isEmpty + else { + fatalError("Invalid receipt payload") + } + + return Data(payload.bytes) + } + + /// Indicates whether the receipt has a valid structure. + /// + /// Returns `true` if the receipt contains a valid payload, signer information, and certificates, + /// `false` otherwise. + public var hasValidStructure: Bool { + guard content.encapContentInfo.eContent != nil, + !content.signerInfos.isEmpty, + !content.certificates.isEmpty + else { + return false + } + + return true + } +} diff --git a/Sources/Blocking/AppReceipt+Blocking.swift b/Sources/Blocking/AppReceipt+Blocking.swift new file mode 100644 index 0000000..00edc04 --- /dev/null +++ b/Sources/Blocking/AppReceipt+Blocking.swift @@ -0,0 +1,144 @@ +import Foundation +import X509 + +@_spi(Blocking) +extension ReceiptValidator { + /// Creates a default receipt validator with blocking verification methods. + /// + /// Initializes a ``ReceiptValidator`` configured with all verifiers + /// to perform complete receipt validation in a blocking manner. + /// + /// - Parameters: + /// - rootCertificate: The root certificate data used to validate the certificate chain. + /// - deviceIdentifier: The device identifier data for hash verification. + /// - Returns: A configured ``ReceiptValidator`` instance ready for receipt validation. + static func default_blocking( + rootCertificate: Data, + deviceIdentifier: Data + ) -> ReceiptValidator { + ReceiptValidator { + SecChainVerifier(rootCertificates: [rootCertificate]) + SecSignatureVerifier() + HashVerifier(deviceIdentifier: deviceIdentifier) + MetaVerifier( + appVersionProvider: nativeAppVersionProvider, + bundleIdentifierProvider: nativeBundleIdentifierProvider + ) + } + } + + /// Validates a receipt using blocking verification methods. + /// + /// Performs synchronous validation of the provided receipt by running it through + /// all configured verifiers sequentially. + /// + /// - Parameter receipt: The receipt object to validate. + /// - Returns: A ``VerificationResult`` indicating whether the receipt is valid or invalid. + public func validate_blocking(_ receipt: any ReceiptValidatable) -> VerificationResult { + for verifier in verifiers { + if case let .invalid(error) = verifier.verify(receipt) { + return .invalid(error) + } + } + + return .valid + } +} + +@_spi(Blocking) +extension Bundle { + fileprivate static func appleRootCertificateData(testing: Bool) -> Data? { + let filename = testing ? "StoreKitTestCertificate.cer" : "AppleIncRootCertificate.cer" + guard let url = url(for: filename) else { + return nil + } + return try? Data(contentsOf: url) + } + + /// Retrieves the local App Store receipt data. + /// + /// Loads the receipt file bundled with the application from the App Store receipt URL. + /// + /// - Returns: The receipt data as ``Data``, or nil if the receipt file cannot be found or loaded. + fileprivate func appStoreReceiptData() -> Data? { + guard let receiptUrl = appStoreReceiptURL else { return nil } + return try? Data(contentsOf: receiptUrl) + } + + /// Retrieve local App Store Receipt Data in base64 string + /// + /// - Returns: ``Data`` object that represents local receipt + /// - throws: An error if receipt file not found or ``Data`` can't be created + public func appStoreReceiptBase64() -> String? { + appStoreReceiptData()?.base64EncodedString() + } +} + +@_spi(Blocking) +extension AppReceipt { + /// Retrieves the local App Store receipt from the application bundle. + /// + /// Loads and parses the receipt file stored locally within the application bundle. + /// + /// - Returns: An ``AppReceipt`` instance. + /// - Throws: ``AppReceiptError/appStoreReceiptNotFound`` if the receipt is not available, + /// or ``AppReceiptError/decodingFailed(_:)`` if the receipt cannot be decoded. + public static var local_blocking: AppReceipt { + get throws(AppReceiptError) { + guard let data = Bundle.main.appStoreReceiptData() else { + throw AppReceiptError.appStoreReceiptNotFound + } + + return try receipt(from: data) + } + } + + /// Validates the receipt using blocking verification methods. + /// + /// Performs synchronous validation of this receipt instance. + /// + /// - Parameters: + /// - rootCertificate: Optional custom root certificate data. If not provided, the appropriate + /// Apple root certificate is selected based on the receipt's environment. + /// - deviceIdentifier: Optional device identifier data. If not provided, the system device + /// identifier is used. + /// - Returns: A ``VerificationResult`` indicating whether the receipt is valid or invalid. + public func validate_blocking( + rootCertificate: Data? = nil, + deviceIdentifier: Data? = nil + ) -> VerificationResult { + guard hasValidStructure else { + return .invalid(ReceiptValidatorError.invalidReceiptStructure) + } + + let root = rootCertificate ?? Bundle.appleRootCertificateData(testing: payload.environment == .xcode) + + guard let root else { + return .invalid(ReceiptValidatorError.rootCertificateInvalid(nil)) + } + + let deviceId = deviceIdentifier ?? DeviceIdentifier.data_blocking + + guard let deviceId else { + return .invalid(HashVerificationError.missingDeviceIdentifier) + } + + let validator = ReceiptValidator.default_blocking( + rootCertificate: root, + deviceIdentifier: deviceId + ) + return validator.validate_blocking(self) + } +} + +extension ReceiptVerifier { + /// Verifies a receipt. + /// + /// This is a synchronous implementation that is not supported. Use the async version instead. + /// + /// - Parameter receipt: The receipt object to verify. + /// - Returns: Never returns; always raises a fatal error. + public func verify(_: any ReceiptValidatable) -> VerificationResult { + fatalError("Use async version instead") + } +} diff --git a/Sources/Bundle+Extension.swift b/Sources/Bundle+Extension.swift deleted file mode 100644 index b39ab6c..0000000 --- a/Sources/Bundle+Extension.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// Bundle+Extension.swift -// TPInAppReceipt -// -// Created by Pavel Tikhonenko on 26.06.2020. -// Copyright © 2020-2021 Pavel Tikhonenko. All rights reserved. -// - -import Foundation - -extension Bundle -{ - /// Appropriate app version for receipt validation - var appVersion: String? - { - #if targetEnvironment(macCatalyst) || os(macOS) - let dictKey: String = "CFBundleShortVersionString" - #else - let dictKey: String = "CFBundleVersion" - #endif - - guard let v = infoDictionary?[dictKey] as? String else - { - return nil - } - - return v - } -} diff --git a/Sources/Bundle+Private.swift b/Sources/Bundle+Private.swift deleted file mode 100644 index 65dc166..0000000 --- a/Sources/Bundle+Private.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Bundle+Private.swift -// TPInAppReceipt -// -// Created by Pavel Tikhonenko on 26.06.2020. -// Copyright © 2020-2021 Pavel Tikhonenko. All rights reserved. -// - -import class Foundation.Bundle -extension Foundation.Bundle -{ - static var module: Bundle = - { - return Bundle(for: _TPInAppReceipt.self) - }() -} - -fileprivate class _TPInAppReceipt {} diff --git a/Sources/Core/AppReceiptField.swift b/Sources/Core/AppReceiptField.swift new file mode 100644 index 0000000..9e4a8ab --- /dev/null +++ b/Sources/Core/AppReceiptField.swift @@ -0,0 +1,137 @@ +/// The field types present in an app receipt ASN.1 structure. +public enum AppReceiptField: Int, Sendable { + /// The receipt's environment (Xcode, Production, or ProductionSandbox). + case environment = 0 + + /// The App Store ID + case appStoreID = 1 + + /// The app's bundle identifier. + case bundleIdentifier = 2 + + /// The app's version number. + case appVersion = 3 + + /// An opaque value used to compute the SHA-1 hash during validation. + case opaqueValue = 4 + + /// The SHA-1 hash used to validate the receipt. + case receiptHash = 5 + + /// Reserved for future use. + case unknown_6 = 6 + + /// Reserved for future use. + case unknown_7 = 7 + + /// The transaction date. + case transactionDate = 8 + + /// The fulfillment tool version. + case fulfillmentToolVersion = 9 + + /// The age rating of the app. + case ageRating = 10 + + /// The developer ID. + case developerID = 11 + + /// The date when the app receipt was created. + case receiptCreationDate = 12 + + /// Reserved for future use. + case unknown_13 = 13 + + /// Reserved for future use. + case unknown_14 = 14 + + /// The download ID. + case downloadID = 15 + + /// The installer version ID. + case installerVersionID = 16 + + /// The receipt for an in-app purchase. + case inAppPurchaseReceipt = 17 + + /// The date when the app was originally purchased. + case originalAppPurchaseDate = 18 + + /// The version of the app that was originally purchased. + case originalAppVersion = 19 + + /// Reserved for future use. + case unknown_20 = 20 + + /// The date that the app receipt expires. + case expirationDate = 21 + + /// Reserved for future use. + case unknown_25 = 25 +} + +/// The field types present in an in-app purchase receipt ASN.1 structure. +public enum InAppPurchaseReceiptField: Int, Sendable { + /// The number of items purchased. + case quantity = 1701 + + /// The product identifier of the item that was purchased. + case productIdentifier = 1702 + + /// The transaction identifier of the item that was purchased. + case transactionIdentifier = 1703 + + /// The date and time that the item was purchased. + case purchaseDate = 1704 + + /// The transaction identifier of the original purchase. + case originalTransactionIdentifier = 1705 + + /// The date of the original transaction. + case originalPurchaseDate = 1706 + + /// The type of in-app product. + case productType = 1707 + + /// The expiration date for the subscription. + case subscriptionExpirationDate = 1708 + + /// Unknown field. + case unknown_1709 = 1709 + + /// Unknown field. + case unknown_1710 = 1710 + + /// The primary key for identifying subscription purchases. + case webOrderLineItemID = 1711 + + /// The time and date of the cancellation. + case cancellationDate = 1712 + + /// Whether the subscription is in the free trial period. + case subscriptionTrialPeriod = 1713 + + /// Unknown field. + case unknown_1714 = 1714 + + /// Unknown field. + case unknown_1715 = 1715 + + /// Unknown field. + case unknown_1716 = 1716 + + /// Unknown field. + case unknown_1717 = 1717 + + /// Unknown field. + case unknown_1718 = 1718 + + /// Whether the subscription is in the introductory price period. + case subscriptionIntroductoryPricePeriod = 1719 + + /// The identifier of the subscription offer the user redeemed. + case promotionalOfferIdentifier = 1721 + + /// Unknown field. + case unknown_1722 = 1722 +} diff --git a/Sources/Core/InAppPurchase.swift b/Sources/Core/InAppPurchase.swift new file mode 100644 index 0000000..507a8af --- /dev/null +++ b/Sources/Core/InAppPurchase.swift @@ -0,0 +1,132 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +/// A record of an in-app purchase transaction. +public struct InAppPurchase: Sendable, Hashable { + /// The type of in-app purchase product. + public enum `Type`: Int32, Sendable { + /// An unknown in-app purchase type. + case unknown = -1 + + /// A non-consumable in-app purchase. + case nonConsumable + + /// A consumable in-app purchase. + case consumable + + /// A non-renewing subscription. + case nonRenewingSubscription + + /// An auto-renewable subscription. + case autoRenewableSubscription + } + + /// The product identifier of the item that was purchased. + public let productIdentifier: String + + /// The type of in-app product. + public let productType: Type + + /// The transaction identifier of the item that was purchased. + public let transactionIdentifier: String + + /// The transaction identifier of the original purchase. + public let originalTransactionIdentifier: String + + /// The date and time that the item was purchased. + public let purchaseDate: Date + + /// The date of the original transaction. + /// + /// Returns `nil` when testing with StoreKitTest. + public let originalPurchaseDate: Date? + + /// The expiration date for the subscription. + /// + /// Only present for auto-renewable subscription receipts. + public let subscriptionExpirationDate: Date? + + /// The date Apple customer support canceled a transaction or the date of an upgrade transaction. + /// + /// Present for auto-renewable subscriptions, non-consumable products, and non-renewing subscriptions + /// when the transaction has been canceled or refunded. Returns `nil` if the purchase has not been + /// canceled or refunded. + public let cancellationDate: Date? + + /// Whether the subscription is in the free trial period. + public let subscriptionTrialPeriod: Bool + + /// Whether the subscription is in the introductory price period. + public let subscriptionIntroductoryPricePeriod: Bool + + /// The primary key for identifying subscription purchases. + /// + /// This value is a unique ID that identifies purchase events across devices, + /// including subscription renewal purchase events. + public let webOrderLineItemID: Int? + + /// The identifier of the subscription offer the user redeemed. + public let promotionalOfferIdentifier: String? + + /// The number of items purchased. + public let quantity: Int + + /// Creates an in-app purchase record with the specified values. + public init( + productIdentifier: String, + productType: Type, + transactionIdentifier: String, + originalTransactionIdentifier: String, + purchaseDate: Date, + originalPurchaseDate: Date?, + subscriptionExpirationDate: Date?, + cancellationDate: Date?, + subscriptionTrialPeriod: Bool, + subscriptionIntroductoryPricePeriod: Bool, + webOrderLineItemID: Int?, + promotionalOfferIdentifier: String?, + quantity: Int + ) { + self.productIdentifier = productIdentifier + self.productType = productType + self.transactionIdentifier = transactionIdentifier + self.originalTransactionIdentifier = originalTransactionIdentifier + self.purchaseDate = purchaseDate + self.originalPurchaseDate = originalPurchaseDate + self.subscriptionExpirationDate = subscriptionExpirationDate + self.cancellationDate = cancellationDate + self.subscriptionTrialPeriod = subscriptionTrialPeriod + self.subscriptionIntroductoryPricePeriod = subscriptionIntroductoryPricePeriod + self.webOrderLineItemID = webOrderLineItemID + self.promotionalOfferIdentifier = promotionalOfferIdentifier + self.quantity = quantity + } +} + +extension InAppPurchase { + /// A Boolean value indicating whether the purchase is a renewable subscription. + public var isRenewableSubscription: Bool { + subscriptionExpirationDate != nil + } + + /// Returns whether the auto-renewable subscription is active for the specified date. + /// + /// - Parameter date: The date to check subscription activity. + /// - Returns: `true` if the subscription is active on the given date, `false` otherwise. + public func isActiveAutoRenewableSubscription(forDate date: Date) -> Bool { + assert(isRenewableSubscription, "\(productIdentifier) is not an auto-renewable subscription.") + + if cancellationDate != nil { + return false + } + + guard let expirationDate = subscriptionExpirationDate else { + return false + } + + return date >= purchaseDate && date < expirationDate + } +} diff --git a/Sources/Core/PCKS7/AlgorithmIdentifier.swift b/Sources/Core/PCKS7/AlgorithmIdentifier.swift new file mode 100644 index 0000000..0fe3194 --- /dev/null +++ b/Sources/Core/PCKS7/AlgorithmIdentifier.swift @@ -0,0 +1,66 @@ +import SwiftASN1 + +/// An algorithm identifier with optional parameters. +/// +/// AlgorithmIdentifier specifies an algorithm and any necessary parameters for its use. +public struct AlgorithmIdentifier: Hashable, Sendable { + /// The object identifier of the algorithm. + public var algorithm: ASN1ObjectIdentifier + + /// The algorithm parameters, if any. + public var parameters: ASN1Any? + + /// Creates an AlgorithmIdentifier with the specified algorithm and parameters. + @inlinable + public init(algorithm: ASN1ObjectIdentifier, parameters: ASN1Any?) { + self.algorithm = algorithm + self.parameters = parameters + } +} + +// MARK: - Algorithm Identifier Statics + +extension AlgorithmIdentifier { + public static let sha1WithRSAEncryption = AlgorithmIdentifier( + algorithm: .AlgorithmIdentifier.sha1WithRSAEncryption, + parameters: try! ASN1Any(erasing: ASN1Null()) + ) + + public static let sha256WithRSAEncryption = AlgorithmIdentifier( + algorithm: .AlgorithmIdentifier.sha256WithRSAEncryption, + parameters: try! ASN1Any(erasing: ASN1Null()) + ) + + public static let sha1 = AlgorithmIdentifier( + algorithm: .AlgorithmIdentifier.sha1, + parameters: try! ASN1Any(erasing: ASN1Null()) + ) + + public static let sha256 = AlgorithmIdentifier( + algorithm: .AlgorithmIdentifier.sha256, + parameters: try! ASN1Any(erasing: ASN1Null()) + ) +} + +extension AlgorithmIdentifier: CustomStringConvertible { + public var description: String { + switch self { + case .sha1WithRSAEncryption: + return "sha1WithRSAEncryption" + case .sha256WithRSAEncryption: + return "sha256WithRSAEncryption" + case .sha1: + return "sha1" + case .sha256: + return "sha256" + default: + return "AlgorithmIdentifier(\(algorithm) - \(String(reflecting: parameters)))" + } + } +} + +extension ASN1ObjectIdentifier.AlgorithmIdentifier { + public static let sha1WithRSAEncryption: ASN1ObjectIdentifier = [1, 2, 840, 113_549, 1, 1, 5] + public static let sha1: ASN1ObjectIdentifier = [1, 3, 14, 3, 2, 26] + public static let sha256: ASN1ObjectIdentifier = [2, 16, 840, 1, 101, 3, 4, 2, 1] +} diff --git a/Sources/Core/PCKS7/Attribute.swift b/Sources/Core/PCKS7/Attribute.swift new file mode 100644 index 0000000..322312f --- /dev/null +++ b/Sources/Core/PCKS7/Attribute.swift @@ -0,0 +1,19 @@ +import SwiftASN1 + +/// A PKCS#7 Attribute structure. +/// +/// An attribute contains a type identifier and associated values. +public struct Attribute: Hashable, Sendable { + /// The object identifier of the attribute type. + public var attrType: ASN1ObjectIdentifier + + /// The attribute values. + public var attrValues: [ASN1Any] + + /// Creates an Attribute with the specified type and values. + @inlinable + public init(attrType: ASN1ObjectIdentifier, attrValues: [ASN1Any]) { + self.attrType = attrType + self.attrValues = attrValues + } +} diff --git a/Sources/Core/PCKS7/ContentInfo.swift b/Sources/Core/PCKS7/ContentInfo.swift new file mode 100644 index 0000000..1a79afe --- /dev/null +++ b/Sources/Core/PCKS7/ContentInfo.swift @@ -0,0 +1,22 @@ +import SwiftASN1 + +/// A PKCS#7 ContentInfo structure. +/// +/// ContentInfo is a simple wrapper that associates content with a content type identifier. +public struct ContentInfo: Sendable { + /// A type alias for the content type identifier. + public typealias ContentType = ASN1ObjectIdentifier + + /// The object identifier of the content type. + public let contentType: ContentType + + /// The content data. + public let content: D + + /// Creates a ContentInfo with the specified type and content. + @inlinable + public init(contentType: ContentType, content: D) { + self.contentType = contentType + self.content = content + } +} diff --git a/Sources/Core/PCKS7/EncapsulatedContentInfo.swift b/Sources/Core/PCKS7/EncapsulatedContentInfo.swift new file mode 100644 index 0000000..97f73d4 --- /dev/null +++ b/Sources/Core/PCKS7/EncapsulatedContentInfo.swift @@ -0,0 +1,27 @@ +import SwiftASN1 + +/// A PKCS#7 EncapsulatedContentInfo structure. +/// +/// EncapsulatedContentInfo contains the type and value of the content being protected. +public struct EncapsulatedContentInfo: Hashable, Sendable { + /// The object identifier of the content type. + public var eContentType: ASN1ObjectIdentifier + + /// The decoded content. + public var eContent: C? + + /// The raw DER-encoded content. + public var eContentRaw: ASN1OctetString? + + /// Creates an EncapsulatedContentInfo with the specified type and content. + @inlinable + public init( + eContentType: ASN1ObjectIdentifier, + eContent: C? = nil, + eContentRaw: ASN1OctetString? = nil + ) { + self.eContentType = eContentType + self.eContent = eContent + self.eContentRaw = eContentRaw + } +} diff --git a/Sources/Core/PCKS7/IssuerAndSerialNumber.swift b/Sources/Core/PCKS7/IssuerAndSerialNumber.swift new file mode 100644 index 0000000..56c6a60 --- /dev/null +++ b/Sources/Core/PCKS7/IssuerAndSerialNumber.swift @@ -0,0 +1,22 @@ +import X509 + +/// The issuer and serial number of a certificate. +/// +/// IssuerAndSerialNumber uniquely identifies a certificate by its issuer's distinguished name and serial number. +public struct IssuerAndSerialNumber: Hashable, Sendable { + /// The issuer's distinguished name. + public var issuer: DistinguishedName + + /// The certificate serial number. + public var serialNumber: Certificate.SerialNumber + + /// Creates an IssuerAndSerialNumber with the specified values. + @inlinable + public init( + issuer: DistinguishedName, + serialNumber: Certificate.SerialNumber + ) { + self.issuer = issuer + self.serialNumber = serialNumber + } +} diff --git a/Sources/Core/PCKS7/SignedData.swift b/Sources/Core/PCKS7/SignedData.swift new file mode 100644 index 0000000..4d3704e --- /dev/null +++ b/Sources/Core/PCKS7/SignedData.swift @@ -0,0 +1,49 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +import X509 + +/// A PKCS#7 SignedData structure. +/// +/// SignedData is the ASN.1 structure defined in RFC 2315 that contains signed content, +/// certificates, and signer information. +public struct SignedData: Hashable, Sendable { + /// The version of the SignedData structure. + public var version: Version + + /// The digest algorithms used by the signers. + public var digestAlgorithms: [AlgorithmIdentifier] + + /// The content that is signed. + public var encapContentInfo: EncapsulatedContentInfo + + /// The X.509 certificates used for validation. + public var certificates: [Certificate] + + /// The raw DER-encoded certificate data. + public var certificatesRaw: [Data] + + /// The signer information and signatures. + public var signerInfos: [SignerInfo] + + /// Creates a SignedData structure with the specified values. + @inlinable + public init( + version: Version, + digestAlgorithms: [AlgorithmIdentifier], + encapContentInfo: EncapsulatedContentInfo, + certificates: [Certificate], + certificatesRaw: [Data] = [], + signerInfos: [SignerInfo] + ) { + self.version = version + self.digestAlgorithms = digestAlgorithms + self.encapContentInfo = encapContentInfo + self.certificates = certificates + self.certificatesRaw = certificatesRaw + self.signerInfos = signerInfos + } +} diff --git a/Sources/Core/PCKS7/SignerIdentifier.swift b/Sources/Core/PCKS7/SignerIdentifier.swift new file mode 100644 index 0000000..e71d871 --- /dev/null +++ b/Sources/Core/PCKS7/SignerIdentifier.swift @@ -0,0 +1,18 @@ +import X509 + +/// An identifier for a signer in a PKCS#7 SignerInfo structure. +/// +/// SignerIdentifier can identify a signer either by issuer and serial number or by subject key identifier. +public enum SignerIdentifier: Hashable, Sendable { + /// The issuer and serial number of the signer's certificate. + case issuerAndSerialNumber(IssuerAndSerialNumber) + + /// The subject key identifier of the signer's certificate. + case subjectKeyIdentifier(SubjectKeyIdentifier) + + /// Creates a signer identifier from a certificate using its issuer and serial number. + @inlinable + public init(issuerAndSerialNumber certificate: Certificate) { + self = .issuerAndSerialNumber(.init(issuer: certificate.issuer, serialNumber: certificate.serialNumber)) + } +} diff --git a/Sources/Core/PCKS7/SignerInfo.swift b/Sources/Core/PCKS7/SignerInfo.swift new file mode 100644 index 0000000..a38b332 --- /dev/null +++ b/Sources/Core/PCKS7/SignerInfo.swift @@ -0,0 +1,53 @@ +import SwiftASN1 + +/// A PKCS#7 SignerInfo structure. +/// +/// SignerInfo contains the signature and signer information for a piece of signed data. +public struct SignerInfo: Hashable, Sendable { + /// Errors that occur in SignerInfo operations. + public enum Error: Swift.Error { + /// The version and signer identifier are incompatible. + case versionAndSignerIdentifierMismatch(String) + } + + /// The version of the SignerInfo structure. + public var version: Version + + /// The identifier of the signer. + public var signerIdentifier: SignerIdentifier + + /// The digest algorithm used by the signer. + public var digestAlgorithm: AlgorithmIdentifier + + /// The signed attributes. + public var signedAttrs: [Attribute]? + + /// The signature algorithm. + public var signatureAlgorithm: AlgorithmIdentifier + + /// The signature value. + public var signature: ASN1OctetString + + /// The unsigned attributes. + public var unsignedAttrs: [Attribute]? + + /// Creates a SignerInfo with the specified values. + @inlinable + public init( + version: Version, + signerIdentifier: SignerIdentifier, + digestAlgorithm: AlgorithmIdentifier, + signedAttrs: [Attribute]? = nil, + signatureAlgorithm: AlgorithmIdentifier, + signature: ASN1OctetString, + unsignedAttrs: [Attribute]? = nil + ) { + self.version = version + self.signerIdentifier = signerIdentifier + self.digestAlgorithm = digestAlgorithm + self.signedAttrs = signedAttrs + self.signatureAlgorithm = signatureAlgorithm + self.signature = signature + self.unsignedAttrs = unsignedAttrs + } +} diff --git a/Sources/Core/PCKS7/Time.swift b/Sources/Core/PCKS7/Time.swift new file mode 100644 index 0000000..7ba0e3c --- /dev/null +++ b/Sources/Core/PCKS7/Time.swift @@ -0,0 +1,66 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +import SwiftASN1 + +/// A time representation in ASN.1 format. +/// +/// Time can be represented in either UTC format (for dates up to 2050) or generalized format (for all dates). +public enum Time: Hashable, Sendable { + /// A UTC time representation. + case utcTime(UTCTime) + + /// A generalized time representation. + case generalTime(GeneralizedTime) +} + +extension GeneralizedTime { + @inlinable + init(_ time: Time) { + switch time { + case let .generalTime(t): + self = t + case let .utcTime(t): + // This can never throw, all valid UTCTimes are valid GeneralizedTimes + self = try! GeneralizedTime( + year: t.year, + month: t.month, + day: t.day, + hours: t.hours, + minutes: t.minutes, + seconds: t.seconds, + fractionalSeconds: 0 + ) + } + } + + @inlinable + init(_ components: (year: Int, month: Int, day: Int, hours: Int, minutes: Int, seconds: Int)) throws { + try self.init( + year: components.year, + month: components.month, + day: components.day, + hours: components.hours, + minutes: components.minutes, + seconds: components.seconds, + fractionalSeconds: 0.0 + ) + } +} + +extension UTCTime { + @inlinable + init(_ components: (year: Int, month: Int, day: Int, hours: Int, minutes: Int, seconds: Int)) throws { + try self.init( + year: components.year, + month: components.month, + day: components.day, + hours: components.hours, + minutes: components.minutes, + seconds: components.seconds + ) + } +} diff --git a/Sources/Core/PCKS7/Version.swift b/Sources/Core/PCKS7/Version.swift new file mode 100644 index 0000000..7e61a60 --- /dev/null +++ b/Sources/Core/PCKS7/Version.swift @@ -0,0 +1,34 @@ +/// A CMS/PKCS#7 version number. +public struct Version: RawRepresentable, Hashable, Sendable { + /// The raw version value. + public var rawValue: Int + + /// Creates a version with the specified raw value. + public init(rawValue: Int) { + self.rawValue = rawValue + } + + /// Version 0. + public static let v0 = Self(rawValue: 0) + + /// Version 1. + public static let v1 = Self(rawValue: 1) + + /// Version 2. + public static let v2 = Self(rawValue: 2) + + /// Version 3. + public static let v3 = Self(rawValue: 3) + + /// Version 4. + public static let v4 = Self(rawValue: 4) + + /// Version 5. + public static let v5 = Self(rawValue: 5) +} + +extension Version: CustomStringConvertible { + public var description: String { + "CMSv\(rawValue)" + } +} diff --git a/Sources/Core/ReceiptPayload.swift b/Sources/Core/ReceiptPayload.swift new file mode 100644 index 0000000..8892c95 --- /dev/null +++ b/Sources/Core/ReceiptPayload.swift @@ -0,0 +1,133 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +/// The payload of an app receipt extracted from a PKCS#7 container. +/// +/// `InAppReceiptPayload` contains all the decoded information from an app receipt, +/// including app details, purchase history, and validation data. +public struct InAppReceiptPayload: Sendable, Hashable { + public enum Environment: String, Hashable, Sendable { + case production = "Production" + case productionSandbox = "ProductionSandbox" + case sandbox = "Sandbox" + case xcode = "Xcode" + case unknown = "Unknown" + + public init(rawValue: String) { + switch rawValue { + case "Production": + self = .production + case "ProductionSandbox": + self = .productionSandbox + case "Sandbox": + self = .sandbox + case "Xcode": + self = .xcode + default: + self = .unknown + } + } + } + + /// The in-app purchase receipts. + public let purchases: [InAppPurchase] + + /// The app's bundle identifier. + public let bundleIdentifier: String + + /// The app's version number. + public let appVersion: String + + /// The version of the app that was originally purchased. + public let originalAppVersion: String? + + /// The date when the app was originally purchased. + public let originalPurchaseDate: Date? + + /// The date that the app receipt expires. + /// + /// This key is only present for apps purchased through the Volume Purchase Program. + public let expirationDate: Date? + + /// The app's bundle identifier as raw data. + /// + /// Used to validate the receipt. + public let bundleIdentifierData: Data + + /// An opaque value used, with other data, to compute the SHA-1 hash. + public let opaqueValue: Data + + /// The SHA-1 hash, used to validate the receipt. + public let receiptHash: Data + + /// The date when the app receipt was created. + public let creationDate: Date + + /// The age rating of the app. + public let ageRating: String? + + /// The receipt's environment. + public let environment: Environment + + /// The Apple ID used to purchase the app. + public let appStoreID: Int? + + /// The transaction date. + public let transactionDate: Date? + + /// The fulfillment tool version. + public let fulfillmentToolVersion: Int? + + /// The developer ID. + public let developerID: Int? + + /// The download ID. + public let downloadID: Int? + + /// The installer version ID. + public let installerVersionID: Int? + + /// Creates an app receipt payload with the specified values. + public init( + bundleIdentifier: String, + appVersion: String, + originalAppVersion: String?, + originalPurchaseDate: Date?, + purchases: [InAppPurchase], + expirationDate: Date?, + bundleIdentifierData: Data, + opaqueValue: Data, + receiptHash: Data, + creationDate: Date, + ageRating: String?, + environment: Environment, + appStoreID: Int? = nil, + transactionDate: Date? = nil, + fulfillmentToolVersion: Int? = nil, + developerID: Int? = nil, + downloadID: Int? = nil, + installerVersionID: Int? = nil + ) { + self.bundleIdentifier = bundleIdentifier + self.appVersion = appVersion + self.originalAppVersion = originalAppVersion + self.originalPurchaseDate = originalPurchaseDate + self.purchases = purchases + self.expirationDate = expirationDate + self.bundleIdentifierData = bundleIdentifierData + self.opaqueValue = opaqueValue + self.receiptHash = receiptHash + self.creationDate = creationDate + self.ageRating = ageRating + self.environment = environment + self.appStoreID = appStoreID + self.transactionDate = transactionDate + self.fulfillmentToolVersion = fulfillmentToolVersion + self.developerID = developerID + self.downloadID = downloadID + self.installerVersionID = installerVersionID + } +} diff --git a/Sources/Data+Extension.swift b/Sources/Data+Extension.swift deleted file mode 100644 index 134c4d1..0000000 --- a/Sources/Data+Extension.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// Data+Extension.swift -// TPReceiptValidator -// -// Created by Pavel Tikhonenko on 29/09/16. -// Copyright © 2016-2021 Pavel Tikhonenko. All rights reserved. -// - -import Foundation - -extension Data -{ - public var bytes: Array - { - return Array(self) - } -} - - diff --git a/Sources/Date+Extension.swift b/Sources/Date+Extension.swift deleted file mode 100644 index d24713c..0000000 --- a/Sources/Date+Extension.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// Date+Extension.swift -// TPInAppReceipt -// -// Created by Pavel Tikhonenko on 01/10/16. -// Copyright © 2016-2021 Pavel Tikhonenko. All rights reserved. -// - -import Foundation - -public extension Date -{ - func rfc3339date(fromString string: String) -> Date? - { - return string.rfc3339date() - } -} - -public extension String -{ - func rfc3339date() -> Date? - { - let date = rfc3339DateFormatter.date(from: self) - return date - } -} - -fileprivate var rfc3339DateFormatter: ISO8601DateFormatter = { - let formatter = ISO8601DateFormatter() - formatter.formatOptions = .withInternetDateTime - formatter.timeZone = TimeZone(abbreviation: "UTC") - return formatter -}() diff --git a/Sources/Decoder/AppReceiptDecoder.swift b/Sources/Decoder/AppReceiptDecoder.swift new file mode 100644 index 0000000..b5d2d11 --- /dev/null +++ b/Sources/Decoder/AppReceiptDecoder.swift @@ -0,0 +1,40 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +/// A decoder for app receipts. +/// +/// `AppReceiptDecoder` uses a decoding engine to extract receipt data. +public final class AppReceiptDecoder: Sendable { + /// A protocol for decoding app receipt data. + /// + /// Implement this protocol to provide custom receipt decoding logic. + public protocol Engine: Sendable { + /// Decodes an app receipt. + /// + /// - Parameter data: The receipt data to decode. + /// - Returns: The decoded app receipt. + /// - Throws: ``AppReceiptError/decodingFailed(_:)`` if the data is invalid. + func decode(from data: Data) throws -> AppReceipt + } + + let engine: Engine + + /// Creates a decoder with the specified decoding engine. + /// + /// - Parameter engine: The engine to use for decoding. + public init(engine: Engine) { + self.engine = engine + } + + /// Decodes an app receipt. + /// + /// - Parameter data: The receipt data to decode. + /// - Returns: The decoded app receipt. + /// - Throws: ``AppReceiptError/decodingFailed(_:)`` if the data is invalid. + func decode(from data: Data) throws -> AppReceipt { + try engine.decode(from: data) + } +} diff --git a/Sources/Decoder/Engine/PCKS7+SwiftASN1/AlgorithmIdentifier+SwiftASN1.swift b/Sources/Decoder/Engine/PCKS7+SwiftASN1/AlgorithmIdentifier+SwiftASN1.swift new file mode 100644 index 0000000..48fe9ed --- /dev/null +++ b/Sources/Decoder/Engine/PCKS7+SwiftASN1/AlgorithmIdentifier+SwiftASN1.swift @@ -0,0 +1,53 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCertificates open source project +// +// Copyright (c) 2022 Apple Inc. and the SwiftCertificates project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCertificates project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import SwiftASN1 + +extension AlgorithmIdentifier: DERSerializable, DERImplicitlyTaggable, BERImplicitlyTaggable { + @inlinable + public static var defaultIdentifier: ASN1Identifier { + .sequence + } + + @inlinable + public init(derEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { + // The AlgorithmIdentifier block looks like this. + // + // AlgorithmIdentifier ::= SEQUENCE { + // algorithm OBJECT IDENTIFIER, + // parameters ANY DEFINED BY algorithm OPTIONAL + // } + self = try DER.sequence(rootNode, identifier: identifier) { nodes in + let algorithmOID = try ASN1ObjectIdentifier(derEncoded: &nodes) + + let parameters = nodes.next().map { ASN1Any(derEncoded: $0) } + + return .init(algorithm: algorithmOID, parameters: parameters) + } + } + + @inlinable + public init(berEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { + self = try .init(derEncoded: rootNode, withIdentifier: identifier) + } + + @inlinable + public func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws { + try coder.appendConstructedNode(identifier: identifier) { coder in + try coder.serialize(self.algorithm) + if let parameters = self.parameters { + try coder.serialize(parameters) + } + } + } +} diff --git a/Sources/Decoder/Engine/PCKS7+SwiftASN1/Attribute+SwiftASN1.swift b/Sources/Decoder/Engine/PCKS7+SwiftASN1/Attribute+SwiftASN1.swift new file mode 100644 index 0000000..2d852cd --- /dev/null +++ b/Sources/Decoder/Engine/PCKS7+SwiftASN1/Attribute+SwiftASN1.swift @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCertificates open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftCertificates project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCertificates project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftASN1 + +/// ``CMSAttribute`` is defined in ASN.1 as: +/// ``` +/// Attribute ::= SEQUENCE { +/// attrType OBJECT IDENTIFIER, +/// attrValues SET OF AttributeValue } +/// +/// AttributeValue ::= ANY +/// ``` +extension Attribute: DERImplicitlyTaggable, BERImplicitlyTaggable { + @inlinable + public static var defaultIdentifier: ASN1Identifier { + .sequence + } + + @inlinable + public init(derEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { + self = try DER.sequence(rootNode, identifier: identifier) { nodes in + let attrType = try ASN1ObjectIdentifier(derEncoded: &nodes) + let attrValues = try DER.set(of: ASN1Any.self, identifier: .set, nodes: &nodes) + + return .init(attrType: attrType, attrValues: attrValues) + } + } + + @inlinable + public init(berEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { + self = try BER.sequence(rootNode, identifier: identifier) { nodes in + let attrType = try ASN1ObjectIdentifier(berEncoded: &nodes) + let attrValues = try BER.set(of: ASN1Any.self, identifier: .set, nodes: &nodes) + + return .init(attrType: attrType, attrValues: attrValues) + } + } + + @inlinable + public func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws { + try coder.appendConstructedNode(identifier: identifier) { coder in + try coder.serialize(self.attrType) + try coder.serializeSetOf(self.attrValues) + } + } +} diff --git a/Sources/Decoder/Engine/PCKS7+SwiftASN1/ContentInfo+SwiftASN1.swift b/Sources/Decoder/Engine/PCKS7+SwiftASN1/ContentInfo+SwiftASN1.swift new file mode 100644 index 0000000..5d8c20e --- /dev/null +++ b/Sources/Decoder/Engine/PCKS7+SwiftASN1/ContentInfo+SwiftASN1.swift @@ -0,0 +1,45 @@ +import SwiftASN1 + +/// ContentInfo ::= SEQUENCE { +/// contentType ContentType, +/// content [0] EXPLICIT ANY DEFINED BY contentType OPTIONAL } +/// +/// ContentType ::= OBJECT IDENTIFIER + +extension ContentInfo: DERParseable where D: BERImplicitlyTaggable {} +extension ContentInfo: DERSerializable where D: BERImplicitlyTaggable {} +extension ContentInfo: DERImplicitlyTaggable where D: BERImplicitlyTaggable {} +extension ContentInfo: BERParseable where D: BERImplicitlyTaggable {} +extension ContentInfo: BERSerializable where D: BERImplicitlyTaggable {} +extension ContentInfo: BERImplicitlyTaggable where D: BERImplicitlyTaggable { + @inlinable + public static var defaultIdentifier: ASN1Identifier { + .sequence + } + + @inlinable + public init(berEncoded node: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { + self = try BER.sequence(node, identifier: identifier) { nodes in + let contentType = try ContentType(berEncoded: &nodes) + + let content = try BER.explicitlyTagged(&nodes, tagNumber: 0, tagClass: .contextSpecific) { node in + try D(berEncoded: node) + } + + return .init(contentType: contentType, content: content) + } + } + + @inlinable + public init(derEncoded node: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { + try self.init(berEncoded: node, withIdentifier: identifier) + } + + @inlinable + public func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws { + try coder.appendConstructedNode(identifier: identifier) { coder in + try coder.serialize(self.contentType) + try coder.serialize(self.content) + } + } +} diff --git a/Sources/Decoder/Engine/PCKS7+SwiftASN1/EncapsulatedContentInfo+SwiftASN1.swift b/Sources/Decoder/Engine/PCKS7+SwiftASN1/EncapsulatedContentInfo+SwiftASN1.swift new file mode 100644 index 0000000..8b3368e --- /dev/null +++ b/Sources/Decoder/Engine/PCKS7+SwiftASN1/EncapsulatedContentInfo+SwiftASN1.swift @@ -0,0 +1,58 @@ +import SwiftASN1 + +/// ``CMSEncapsulatedContentInfo`` is defined in ASN.1 as: +/// ``` +/// EncapsulatedContentInfo ::= SEQUENCE { +/// eContentType ContentType, +/// eContent [0] EXPLICIT OCTET STRING OPTIONAL } +/// ContentType ::= OBJECT IDENTIFIER +/// ``` +/// +extension EncapsulatedContentInfo: DERParseable where C: BERImplicitlyTaggable {} +extension EncapsulatedContentInfo: DERSerializable where C: BERImplicitlyTaggable {} +extension EncapsulatedContentInfo: DERImplicitlyTaggable where C: BERImplicitlyTaggable {} +extension EncapsulatedContentInfo: BERParseable where C: BERImplicitlyTaggable {} +extension EncapsulatedContentInfo: BERSerializable where C: BERImplicitlyTaggable {} +extension EncapsulatedContentInfo: BERImplicitlyTaggable where C: BERImplicitlyTaggable { + @inlinable + public static var defaultIdentifier: ASN1Identifier { + .sequence + } + + @inlinable + public init(berEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { + self = try BER.sequence(rootNode, identifier: identifier) { nodes in + let eContentType = try ASN1ObjectIdentifier(berEncoded: &nodes) + let eContentRaw = try BER.optionalExplicitlyTagged(&nodes, tagNumber: 0, tagClass: .contextSpecific) { + node in + try ASN1OctetString(berEncoded: node) + } + + guard let eContentRaw else { + return .init(eContentType: eContentType) + } + let eContent = try C(berEncoded: eContentRaw.bytes) + return .init(eContentType: eContentType, eContent: eContent, eContentRaw: eContentRaw) + } + } + + @inlinable + public init(derEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { + try self.init(berEncoded: rootNode, withIdentifier: identifier) + } + + @inlinable + public func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws { + try coder.appendConstructedNode( + identifier: identifier, + { coder in + try coder.serialize(eContentType) + if let eContent { + try coder.serialize(explicitlyTaggedWithTagNumber: 0, tagClass: .contextSpecific) { coder in + try coder.serialize(eContent) + } + } + } + ) + } +} diff --git a/Sources/Decoder/Engine/PCKS7+SwiftASN1/InAppPurchase+SwiftASN1.swift b/Sources/Decoder/Engine/PCKS7+SwiftASN1/InAppPurchase+SwiftASN1.swift new file mode 100644 index 0000000..6c5b201 --- /dev/null +++ b/Sources/Decoder/Engine/PCKS7+SwiftASN1/InAppPurchase+SwiftASN1.swift @@ -0,0 +1,109 @@ +import Foundation +import SwiftASN1 + +extension InAppPurchase: BERImplicitlyTaggable { + public static let defaultIdentifier: SwiftASN1.ASN1Identifier = .set + + public init(berEncoded: SwiftASN1.ASN1Node, withIdentifier identifier: SwiftASN1.ASN1Identifier) throws { + let attributes = try BER.set( + of: InAppReceiptPayloadAttribute.self, + identifier: identifier, + rootNode: berEncoded + ) + try self.init(set: attributes) + } + + public init(derEncoded: SwiftASN1.ASN1Node, withIdentifier identifier: SwiftASN1.ASN1Identifier) throws { + try self.init(berEncoded: derEncoded, withIdentifier: identifier) + } + + public func serialize(into _: inout DER.Serializer, withIdentifier _: SwiftASN1.ASN1Identifier) throws {} + + public init(set: [InAppReceiptPayloadAttribute]) throws { + var originalTransactionIdentifier = "" + var productIdentifier = "" + var transactionIdentifier = "" + var purchaseDate: Date! + var originalPurchaseDate: Date? + var quantity = 1 + var productType: InAppPurchase.`Type` = .unknown + var subscriptionExpirationDate: Date? = nil + var cancellationDate: Date? = nil + var webOrderLineItemID: Int? = nil + var subscriptionTrialPeriod = false + var subscriptionIntroductoryPricePeriod = false + var promotionalOfferIdentifier: String? = nil + + for att in set { + guard let field = InAppPurchaseReceiptField(rawValue: att.type) else { continue } + + switch field { + case .quantity: + let node = try DER.parse(att.value.bytes) + quantity = try Int(derEncoded: node) + + case .productIdentifier: + productIdentifier = try String(att.value, as: .utf8) + + case .productType: + let node = try DER.parse(att.value.bytes) + let typeValue = try Int32(derEncoded: node) + productType = Type(rawValue: typeValue) ?? .unknown + + case .transactionIdentifier: + transactionIdentifier = (try? String(att.value, as: .utf8)) ?? "" + + case .purchaseDate: + purchaseDate = try Date(att.value) + + case .originalTransactionIdentifier: + originalTransactionIdentifier = try String(att.value, as: .utf8) + + case .originalPurchaseDate: + originalPurchaseDate = try Date(att.value) + + case .subscriptionExpirationDate: + subscriptionExpirationDate = try? Date(att.value) + + case .cancellationDate: + cancellationDate = try? Date(att.value) + + case .webOrderLineItemID: + let node = try DER.parse(att.value.bytes) + webOrderLineItemID = try Int(derEncoded: node) + + case .subscriptionTrialPeriod: + let node = try DER.parse(att.value.bytes) + let value = try Int(derEncoded: node) + subscriptionTrialPeriod = value != 0 + + case .subscriptionIntroductoryPricePeriod: + let node = try DER.parse(att.value.bytes) + let value = try Int(derEncoded: node) + subscriptionIntroductoryPricePeriod = value != 0 + + case .promotionalOfferIdentifier: + promotionalOfferIdentifier = try? String(att.value, as: .utf8) + + default: + continue + } + } + + self.init( + productIdentifier: productIdentifier, + productType: productType, + transactionIdentifier: transactionIdentifier, + originalTransactionIdentifier: originalTransactionIdentifier, + purchaseDate: purchaseDate, + originalPurchaseDate: originalPurchaseDate, + subscriptionExpirationDate: subscriptionExpirationDate, + cancellationDate: cancellationDate, + subscriptionTrialPeriod: subscriptionTrialPeriod, + subscriptionIntroductoryPricePeriod: subscriptionIntroductoryPricePeriod, + webOrderLineItemID: webOrderLineItemID, + promotionalOfferIdentifier: promotionalOfferIdentifier, + quantity: quantity + ) + } +} diff --git a/Sources/Decoder/Engine/PCKS7+SwiftASN1/InAppReceiptPayload+SwiftASN1.swift b/Sources/Decoder/Engine/PCKS7+SwiftASN1/InAppReceiptPayload+SwiftASN1.swift new file mode 100644 index 0000000..b685fc0 --- /dev/null +++ b/Sources/Decoder/Engine/PCKS7+SwiftASN1/InAppReceiptPayload+SwiftASN1.swift @@ -0,0 +1,181 @@ +import Foundation +import SwiftASN1 + +/// ReceiptAttribute ::= SEQUENCE { +/// type INTEGER, +/// version INTEGER, +/// value OCTET STRING +/// } +public struct InAppReceiptPayloadAttribute: BERImplicitlyTaggable { + public static let defaultIdentifier: SwiftASN1.ASN1Identifier = .sequence + + let type: Int + let version: Int + let value: ASN1OctetString + + public init(type: Int, version: Int, value: ASN1OctetString) { + self.type = type + self.version = version + self.value = value + } + + public init(berEncoded: SwiftASN1.ASN1Node, withIdentifier identifier: SwiftASN1.ASN1Identifier) throws { + self = try BER.sequence(berEncoded, identifier: identifier) { nodes in + let type = try Int(berEncoded: &nodes) + let version = try Int(berEncoded: &nodes) + let octetString = try ASN1OctetString(berEncoded: &nodes) + + return .init(type: type, version: version, value: octetString) + } + } + + public init(derEncoded: SwiftASN1.ASN1Node, withIdentifier identifier: SwiftASN1.ASN1Identifier) throws { + try self.init(berEncoded: derEncoded, withIdentifier: identifier) + } + + public func serialize( + into coder: inout SwiftASN1.DER.Serializer, + withIdentifier identifier: SwiftASN1.ASN1Identifier + ) throws { + try coder.appendConstructedNode(identifier: identifier) { coder in + try coder.serialize(type) + try coder.serialize(version) + try coder.serialize(value) + } + } +} + +extension InAppReceiptPayload: BERImplicitlyTaggable { + public static var defaultIdentifier: SwiftASN1.ASN1Identifier { + .set + } + + public init(berEncoded: SwiftASN1.ASN1Node, withIdentifier identifier: SwiftASN1.ASN1Identifier) throws { + let attributes = try BER.set( + of: InAppReceiptPayloadAttribute.self, + identifier: identifier, + rootNode: berEncoded + ) + try self.init(set: attributes, rawBytes: berEncoded.encodedBytes) + } + + public init(derEncoded: SwiftASN1.ASN1Node, withIdentifier identifier: SwiftASN1.ASN1Identifier) throws { + try self.init(berEncoded: derEncoded, withIdentifier: identifier) + } + + public func serialize(into _: inout SwiftASN1.DER.Serializer, withIdentifier _: SwiftASN1.ASN1Identifier) throws { + fatalError("TPInAppReceipt doesn't support encoding") + } + + public init(set: [InAppReceiptPayloadAttribute], rawBytes _: ArraySlice) throws { + var bundleIdentifier: String! + var bundleIdentifierData: Data! + var appVersion: String! + var originalAppVersion: String! + var originalPurchaseDate: Date? + var purchases: [InAppPurchase] = [] + var opaqueValue: Data! + var receiptHash: Data! + var expirationDate: Date? + var receiptCreationDate: Date! + var ageRating: String! + var environment: String! + var appStoreID: Int? + var transactionDate: Date? + var fulfillmentToolVersion: Int? + var developerID: Int? + var downloadID: Int? + var installerVersionID: Int? + + for att in set { + guard let field = AppReceiptField(rawValue: att.type) else { continue } + + switch field { + case .bundleIdentifier: + bundleIdentifier = try String(att.value, as: .utf8) + bundleIdentifierData = Data(att.value.bytes) + case .appVersion: + appVersion = try String(att.value, as: .utf8) + case .opaqueValue: + opaqueValue = Data(att.value.bytes) + case .receiptHash: + receiptHash = Data(att.value.bytes) + case .inAppPurchaseReceipt: + try purchases.append(InAppPurchase(berEncoded: att.value.bytes)) + case .originalAppVersion: + originalAppVersion = try String(att.value, as: .utf8) + case .originalAppPurchaseDate: + originalPurchaseDate = try Date(att.value) + case .expirationDate: + expirationDate = try Date(att.value) + case .receiptCreationDate: + receiptCreationDate = try Date(att.value) + case .ageRating: + ageRating = try String(att.value, as: .ascii) + case .environment: + environment = try String(att.value, as: .utf8) + case .appStoreID: + let node = try DER.parse(att.value.bytes) + appStoreID = try Int(derEncoded: node) + case .transactionDate: + transactionDate = try? Date(att.value) + case .fulfillmentToolVersion: + fulfillmentToolVersion = try Int(derEncoded: att.value.bytes) + case .developerID: + developerID = try Int(derEncoded: att.value.bytes) + case .downloadID: + downloadID = try Int(derEncoded: att.value.bytes) + case .installerVersionID: + installerVersionID = try Int(derEncoded: att.value.bytes) + default: + break + } + } + + self.init( + bundleIdentifier: bundleIdentifier, + appVersion: appVersion, + originalAppVersion: originalAppVersion, + originalPurchaseDate: originalPurchaseDate, + purchases: purchases, + expirationDate: expirationDate, + bundleIdentifierData: bundleIdentifierData, + opaqueValue: opaqueValue, + receiptHash: receiptHash, + creationDate: receiptCreationDate, + ageRating: ageRating, + environment: Environment(rawValue: environment), + appStoreID: appStoreID, + transactionDate: transactionDate, + fulfillmentToolVersion: fulfillmentToolVersion, + developerID: developerID, + downloadID: downloadID, + installerVersionID: installerVersionID + ) + } +} + +extension String { + init(_ octetString: ASN1OctetString, as sourceEncoding: Encoding) throws { + switch sourceEncoding { + case Encoding.utf8: + self = try String(ASN1UTF8String(derEncoded: octetString.bytes)) + case Encoding.ascii: + self = try String(ASN1IA5String(derEncoded: octetString.bytes)) + default: + fatalError("Unhandled string source encoding: \(sourceEncoding.rawValue)") + } + } +} + +extension Date { + init(_ octetString: ASN1OctetString) throws { + let str = try ASN1IA5String(berEncoded: octetString.bytes) + + guard let date = Date.date(from: String(str)) else { + throw ASN1Error.invalidASN1Object(reason: "Invalid date format") + } + + self = date + } +} diff --git a/Sources/Decoder/Engine/PCKS7+SwiftASN1/IssuerAndSerialNumber+SwiftASN1.swift b/Sources/Decoder/Engine/PCKS7+SwiftASN1/IssuerAndSerialNumber+SwiftASN1.swift new file mode 100644 index 0000000..97aadf2 --- /dev/null +++ b/Sources/Decoder/Engine/PCKS7+SwiftASN1/IssuerAndSerialNumber+SwiftASN1.swift @@ -0,0 +1,47 @@ +import SwiftASN1 +import X509 + +/// ``IssuerAndSerialNumber`` is defined in ASN.1 as: +/// ``` +/// IssuerAndSerialNumber ::= SEQUENCE { +/// issuer Name, +/// serialNumber CertificateSerialNumber } +/// ``` +/// The definition of `Name` is taken from X.501 [X.501-88], and the +/// definition of `CertificateSerialNumber` is taken from X.509 [X.509-97]. +/// + +extension IssuerAndSerialNumber: DERImplicitlyTaggable { + @inlinable + public static var defaultIdentifier: ASN1Identifier { + .sequence + } + + @inlinable + public init(derEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { + self = try DER.sequence(rootNode, identifier: identifier) { nodes in + let issuer = try DistinguishedName.derEncoded(&nodes) + let serialNumber = try ArraySlice(derEncoded: &nodes) + return .init(issuer: issuer, serialNumber: .init(bytes: serialNumber)) + } + } + + @inlinable + public func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws { + try coder.appendConstructedNode(identifier: identifier) { coder in + try coder.serialize(self.issuer) + try coder.serialize(self.serialNumber.bytes) + } + } +} + +extension DistinguishedName { + @inlinable + static func derEncoded(_ sequenceNodeIterator: inout ASN1NodeCollection.Iterator) throws -> DistinguishedName { + // This is a workaround for the fact that, even though the conformance to DERImplicitlyTaggable is + // deprecated, Swift still prefers calling init(derEncoded:withIdentifier:) instead of this one. + let dnFactory: (inout ASN1NodeCollection.Iterator) throws -> DistinguishedName = + DistinguishedName.init(derEncoded:) + return try dnFactory(&sequenceNodeIterator) + } +} diff --git a/Sources/Decoder/Engine/PCKS7+SwiftASN1/SignedData+SwiftASN1.swift b/Sources/Decoder/Engine/PCKS7+SwiftASN1/SignedData+SwiftASN1.swift new file mode 100644 index 0000000..1ae704f --- /dev/null +++ b/Sources/Decoder/Engine/PCKS7+SwiftASN1/SignedData+SwiftASN1.swift @@ -0,0 +1,103 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +import SwiftASN1 +import X509 + +/// ``SignedData`` is defined in ASN.1 as: +/// ``` +/// SignedData ::= SEQUENCE { +/// version CMSVersion, +/// digestAlgorithms DigestAlgorithmIdentifiers, +/// encapContentInfo EncapsulatedContentInfo, +/// certificates [0] IMPLICIT CertificateSet OPTIONAL, +/// crls [1] IMPLICIT RevocationInfoChoices OPTIONAL, +/// signerInfos SignerInfos } +/// +/// DigestAlgorithmIdentifiers ::= SET OF DigestAlgorithmIdentifier +/// DigestAlgorithmIdentifier ::= AlgorithmIdentifier +/// SignerInfos ::= SET OF SignerInfo +/// CertificateSet ::= SET OF CertificateChoices +/// +/// CertificateChoices ::= CHOICE { +/// certificate Certificate, +/// extendedCertificate [0] IMPLICIT ExtendedCertificate, -- Obsolete +/// v1AttrCert [1] IMPLICIT AttributeCertificateV1, -- Obsolete +/// v2AttrCert [2] IMPLICIT AttributeCertificateV2, +/// other [3] IMPLICIT OtherCertificateFormat } +/// +/// OtherCertificateFormat ::= SEQUENCE { +/// otherCertFormat OBJECT IDENTIFIER, +/// otherCert ANY DEFINED BY otherCertFormat } +/// ``` +/// - Note: At the moment we don't support `crls` (`RevocationInfoChoices`) +/// +extension SignedData: DERParseable where C: BERImplicitlyTaggable {} +extension SignedData: DERSerializable where C: BERImplicitlyTaggable {} +extension SignedData: DERImplicitlyTaggable where C: BERImplicitlyTaggable {} +extension SignedData: BERParseable where C: BERImplicitlyTaggable {} +extension SignedData: BERSerializable where C: BERImplicitlyTaggable {} +extension SignedData: BERImplicitlyTaggable where C: BERImplicitlyTaggable { + @inlinable + public static var defaultIdentifier: SwiftASN1.ASN1Identifier { + .sequence + } + + @inlinable + public init(berEncoded: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { + self = try BER.sequence(berEncoded, identifier: identifier) { nodes in + let version = try Version(rawValue: Int(berEncoded: &nodes)) + let digestAlgorithms = try BER.set(of: AlgorithmIdentifier.self, identifier: .set, nodes: &nodes) + + let encapContentInfo = try EncapsulatedContentInfo(berEncoded: &nodes) + var certificatesRaw: [Data] = [] + let certificates = try BER.optionalImplicitlyTagged(&nodes, tagNumber: 0, tagClass: .contextSpecific) { + node in + try BER.set(node, identifier: .init(tagWithNumber: 0, tagClass: .contextSpecific)) { iter in + var res: [Certificate] = [] + while let next = iter.next() { + try res.append(Certificate(derEncoded: next)) + certificatesRaw.append(Data(next.encodedBytes)) + } + return res + } + } + + // we need to skip this node even though we don't support it + _ = BER.optionalImplicitlyTagged(&nodes, tagNumber: 1, tagClass: .contextSpecific) { _ in } + + let signerInfos = try BER.set(of: SignerInfo.self, identifier: .set, nodes: &nodes) + + return .init( + version: version, + digestAlgorithms: digestAlgorithms, + encapContentInfo: encapContentInfo, + certificates: certificates ?? [], + certificatesRaw: certificatesRaw, + signerInfos: signerInfos + ) + } + } + + @inlinable + public init(derEncoded: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { + try self.init(berEncoded: derEncoded, withIdentifier: identifier) + } + + @inlinable + public func serialize(into coder: inout DER.Serializer, withIdentifier identifier: SwiftASN1.ASN1Identifier) throws + { + try coder.appendConstructedNode(identifier: identifier) { coder in + try coder.serialize(version.rawValue) + try coder.serializeSetOf(self.digestAlgorithms) + try coder.serialize(self.encapContentInfo) + if !certificates.isEmpty { + try coder.serializeSetOf(certificates, identifier: .init(tagWithNumber: 0, tagClass: .contextSpecific)) + } + try coder.serializeSetOf(self.signerInfos, identifier: .set) + } + } +} diff --git a/Sources/Decoder/Engine/PCKS7+SwiftASN1/SignerIdentifier+SwiftASN1.swift b/Sources/Decoder/Engine/PCKS7+SwiftASN1/SignerIdentifier+SwiftASN1.swift new file mode 100644 index 0000000..b928fc6 --- /dev/null +++ b/Sources/Decoder/Engine/PCKS7+SwiftASN1/SignerIdentifier+SwiftASN1.swift @@ -0,0 +1,61 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCertificates open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftCertificates project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCertificates project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftASN1 +import X509 + +/// ``SignerIdentifier`` is defined in ASN.1 as: +/// ``` +/// SignerIdentifier ::= CHOICE { +/// issuerAndSerialNumber IssuerAndSerialNumber, +/// subjectKeyIdentifier [0] SubjectKeyIdentifier } +/// ``` +/// +extension SignerIdentifier: DERParseable, BERParseable, DERSerializable, BERSerializable { + @usableFromInline + static let skiIdentifier = ASN1Identifier(tagWithNumber: 0, tagClass: .contextSpecific) + + @inlinable + public init(derEncoded node: ASN1Node) throws { + switch node.identifier { + case IssuerAndSerialNumber.defaultIdentifier: + self = try .issuerAndSerialNumber(.init(derEncoded: node)) + + case Self.skiIdentifier: + self = try .subjectKeyIdentifier( + .init( + keyIdentifier: ASN1OctetString( + derEncoded: node, + withIdentifier: Self.skiIdentifier + ).bytes + ) + ) + + default: + throw ASN1Error.unexpectedFieldType(node.identifier) + } + } + + @inlinable + public func serialize(into coder: inout DER.Serializer) throws { + switch self { + case let .issuerAndSerialNumber(issuerAndSerialNumber): + try issuerAndSerialNumber.serialize(into: &coder) + + case let .subjectKeyIdentifier(subjectKeyIdentifier): + try ASN1OctetString(contentBytes: subjectKeyIdentifier.keyIdentifier) + .serialize(into: &coder, withIdentifier: Self.skiIdentifier) + } + } +} diff --git a/Sources/Decoder/Engine/PCKS7+SwiftASN1/SignerInfo+SwiftASN1.swift b/Sources/Decoder/Engine/PCKS7+SwiftASN1/SignerInfo+SwiftASN1.swift new file mode 100644 index 0000000..5dc062b --- /dev/null +++ b/Sources/Decoder/Engine/PCKS7+SwiftASN1/SignerInfo+SwiftASN1.swift @@ -0,0 +1,195 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCertificates open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftCertificates project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCertificates project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif +import SwiftASN1 + +/// ``SignerInfo`` is defined in ASN.1 as: +/// ``` +/// SignerInfo ::= SEQUENCE { +/// version CMSVersion, +/// sid SignerIdentifier, +/// digestAlgorithm DigestAlgorithmIdentifier, +/// signedAttrs [0] IMPLICIT SignedAttributes OPTIONAL, +/// signatureAlgorithm SignatureAlgorithmIdentifier, +/// signature SignatureValue, +/// unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL } +/// +/// SignatureValue ::= OCTET STRING +/// DigestAlgorithmIdentifier ::= AlgorithmIdentifier +/// SignatureAlgorithmIdentifier ::= AlgorithmIdentifier +/// ``` +/// - Note: If the `SignerIdentifier` is the CHOICE `issuerAndSerialNumber`, +/// then the `version` MUST be 1. If the `SignerIdentifier` is `subjectKeyIdentifier`, +/// then the `version` MUST be 3. +/// +extension SignerInfo: DERImplicitlyTaggable, BERImplicitlyTaggable { + @inlinable + public static var defaultIdentifier: ASN1Identifier { + .sequence + } + + @inlinable + public init(derEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { + self = try DER.sequence(rootNode, identifier: identifier) { nodes in + let version = try Version(rawValue: Int(derEncoded: &nodes)) + let signerIdentifier = try SignerIdentifier(derEncoded: &nodes) + switch signerIdentifier { + case .issuerAndSerialNumber: + guard version == .v1 else { + throw Error.versionAndSignerIdentifierMismatch( + "expected \(Version.v1) but got \(version) where signerIdentifier is \(signerIdentifier)" + ) + } + case .subjectKeyIdentifier: + guard version == .v3 else { + throw Error.versionAndSignerIdentifierMismatch( + "expected \(Version.v3) but got \(version) where signerIdentifier is \(signerIdentifier)" + ) + } + } + let digestAlgorithm = try AlgorithmIdentifier(derEncoded: &nodes) + + let signedAttrs = try DER.optionalImplicitlyTagged(&nodes, tagNumber: 0, tagClass: .contextSpecific) { + node in + try DER.set( + of: Attribute.self, + identifier: .init(tagWithNumber: 0, tagClass: .contextSpecific), + rootNode: node + ) + } + + let signatureAlgorithm = try AlgorithmIdentifier(derEncoded: &nodes) + let signature = try ASN1OctetString(derEncoded: &nodes) + + let unsignedAttrs = try DER.optionalImplicitlyTagged(&nodes, tagNumber: 1, tagClass: .contextSpecific) { + node in + try DER.set( + of: Attribute.self, + identifier: .init(tagWithNumber: 0, tagClass: .contextSpecific), + rootNode: node + ) + } + + return .init( + version: version, + signerIdentifier: signerIdentifier, + digestAlgorithm: digestAlgorithm, + signedAttrs: signedAttrs, + signatureAlgorithm: signatureAlgorithm, + signature: signature, + unsignedAttrs: unsignedAttrs + ) + } + } + + @inlinable + public init(berEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { + self = try BER.sequence(rootNode, identifier: identifier) { nodes in + let version = try Version(rawValue: Int(derEncoded: &nodes)) + let signerIdentifier = try SignerIdentifier(berEncoded: &nodes) + switch signerIdentifier { + case .issuerAndSerialNumber: + guard version == .v1 else { + throw Error.versionAndSignerIdentifierMismatch( + "expected \(Version.v1) but got \(version) where signerIdentifier is \(signerIdentifier)" + ) + } + case .subjectKeyIdentifier: + guard version == .v3 else { + throw Error.versionAndSignerIdentifierMismatch( + "expected \(Version.v3) but got \(version) where signerIdentifier is \(signerIdentifier)" + ) + } + } + let digestAlgorithm = try AlgorithmIdentifier(berEncoded: &nodes) + + // SignedAttrs MUST be in DER: https://datatracker.ietf.org/doc/html/rfc5652#section-2 + let signedAttrs = try DER.optionalImplicitlyTagged(&nodes, tagNumber: 0, tagClass: .contextSpecific) { + node in + try DER.set( + of: Attribute.self, + identifier: .init(tagWithNumber: 0, tagClass: .contextSpecific), + rootNode: node + ) + } + + let signatureAlgorithm = try AlgorithmIdentifier(berEncoded: &nodes) + let signature = try ASN1OctetString(berEncoded: &nodes) + + let unsignedAttrs = try BER.optionalImplicitlyTagged(&nodes, tagNumber: 1, tagClass: .contextSpecific) { + node in + try BER.set( + of: Attribute.self, + identifier: .init(tagWithNumber: 0, tagClass: .contextSpecific), + rootNode: node + ) + } + + return .init( + version: version, + signerIdentifier: signerIdentifier, + digestAlgorithm: digestAlgorithm, + signedAttrs: signedAttrs, + signatureAlgorithm: signatureAlgorithm, + signature: signature, + unsignedAttrs: unsignedAttrs + ) + } + } + + @inlinable + public func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws { + try coder.appendConstructedNode(identifier: identifier) { coder in + try coder.serialize(self.version.rawValue) + try coder.serialize(self.signerIdentifier) + try coder.serialize(self.digestAlgorithm) + if let signedAttrs = self.signedAttrs { + try coder.serializeSetOf(signedAttrs, identifier: .init(tagWithNumber: 0, tagClass: .contextSpecific)) + } + try coder.serialize(self.signatureAlgorithm) + try coder.serialize(self.signature) + if let unsignedAttrs = self.unsignedAttrs { + try coder.serializeSetOf(unsignedAttrs, identifier: .init(tagWithNumber: 1, tagClass: .contextSpecific)) + } + } + } +} + +// MARK: - Attribute Getters + +extension Array where Element == Attribute { + @inlinable + subscript(oid: ASN1ObjectIdentifier) -> Attribute? { + if let attr = first(where: { $0.attrType == oid }) { + return attr + } + return nil + } +} + +extension ASN1ObjectIdentifier { + @usableFromInline + static let messageDigest: Self = [1, 2, 840, 113_549, 1, 9, 4] + + @usableFromInline + static let signingTime: Self = [1, 2, 840, 113_549, 1, 9, 5] + + @usableFromInline + static let contentType: Self = [1, 2, 840, 113_549, 1, 9, 3] +} diff --git a/Sources/Decoder/Engine/PCKS7+SwiftASN1/Time+SwiftASN1.swift b/Sources/Decoder/Engine/PCKS7+SwiftASN1/Time+SwiftASN1.swift new file mode 100644 index 0000000..086ad20 --- /dev/null +++ b/Sources/Decoder/Engine/PCKS7+SwiftASN1/Time+SwiftASN1.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCertificates open source project +// +// Copyright (c) 2022-2023 Apple Inc. and the SwiftCertificates project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCertificates project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif +import SwiftASN1 + +// Time ::= CHOICE { +// utcTime UTCTime, +// generalTime GeneralizedTime } +extension Time: DERParseable, DERSerializable { + @inlinable + public init(derEncoded rootNode: ASN1Node) throws { + switch rootNode.identifier { + case GeneralizedTime.defaultIdentifier: + self = try .generalTime(GeneralizedTime(derEncoded: rootNode)) + case UTCTime.defaultIdentifier: + self = try .utcTime(UTCTime(derEncoded: rootNode)) + default: + throw ASN1Error.unexpectedFieldType(rootNode.identifier) + } + } + + @inlinable + public func serialize(into coder: inout DER.Serializer) throws { + switch self { + case let .utcTime(utcTime): + try coder.serialize(utcTime) + case let .generalTime(generalizedTime): + try coder.serialize(generalizedTime) + } + } +} diff --git a/Sources/Decoder/Engine/SwiftASN1ReceiptDecoder.swift b/Sources/Decoder/Engine/SwiftASN1ReceiptDecoder.swift new file mode 100644 index 0000000..83b1293 --- /dev/null +++ b/Sources/Decoder/Engine/SwiftASN1ReceiptDecoder.swift @@ -0,0 +1,31 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +import SwiftASN1 + +public final class SwiftASN1ReceiptDecoder: AppReceiptDecoder.Engine { + public func decode(from data: Data) throws -> AppReceipt { + try AppReceipt(berEncoded: data) + } +} + +extension BERParseable { + public init(berEncoded data: Data) throws { + try self.init(berEncoded: data.bytes) + } +} + +extension DERParseable { + public init(derEncoded data: Data) throws { + try self.init(derEncoded: data.bytes) + } +} + +extension Data { + fileprivate var bytes: [UInt8] { + Array(self) + } +} diff --git a/Sources/Decoding.swift b/Sources/Decoding.swift new file mode 100644 index 0000000..f56cec0 --- /dev/null +++ b/Sources/Decoding.swift @@ -0,0 +1,52 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +extension AppReceiptDecoder { + /// The default receipt decoder using SwiftASN1. + /// + /// Creates a pre-configured decoder instance that uses the SwiftASN1 engine + /// for decoding App Store receipts. + public static var `default`: Self { + .init(engine: SwiftASN1ReceiptDecoder()) + } +} + +extension AppReceipt { + /// Creates and decodes an ``AppReceipt`` instance from raw receipt data. + /// + /// Decodes the provided receipt data using the default ASN.1 decoder to create + /// a fully parsed ``AppReceipt`` instance. + /// + /// - Parameter data: The raw receipt data (typically from the app bundle). + /// - Returns: A decoded ``AppReceipt`` instance. + /// - Throws: ``AppReceiptError/decodingFailed(_:)`` if the receipt data is invalid or cannot be decoded. + public static func receipt(from data: Data) throws(AppReceiptError) -> AppReceipt { + do { + return try AppReceiptDecoder.default.decode(from: data) + } catch let error as AppReceiptError { + throw error + } catch { + throw .decodingFailed(error) + } + } + + /// Retrieves and decodes the local App Store receipt asynchronously. + /// + /// Loads the receipt bundled with the application from the App Store receipt URL and decodes it. + /// + /// - Returns: A decoded ``AppReceipt`` instance. + /// - Throws: ``AppReceiptError/appStoreReceiptNotFound`` if the receipt is not available, + /// or ``AppReceiptError/decodingFailed(_:)`` if the receipt cannot be decoded. + public static var local: AppReceipt { + get async throws(AppReceiptError) { + guard let data = await Bundle.main.appStoreReceiptData() else { + throw AppReceiptError.appStoreReceiptNotFound + } + + return try receipt(from: data) + } + } +} diff --git a/Sources/Docs.docc/Architecture.md b/Sources/Docs.docc/Architecture.md new file mode 100644 index 0000000..58418ec --- /dev/null +++ b/Sources/Docs.docc/Architecture.md @@ -0,0 +1,133 @@ +# Architecture + +How the library is structured and where to plug in custom implementations. + +## Overview + +The library has three layers: **Core** (data models), **Decoder** (parsing), and **Validator** (verification). Both Decoder and Validator are pluggable. + +``` +┌──────────────────────────────────────────────┐ +│ Public API │ +│ AppReceipt extensions │ +├──────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌──────────────────────┐ │ +│ │ Decoder │ │ Validator │ │ +│ │ │ │ │ │ +│ │ Engine │ │ ReceiptValidator │ │ +│ │ protocol │ │ @VerifierBuilder │ │ +│ │ │ │ │ │ +│ │ SwiftASN1 │ │ X509ChainVerifier │ │ +│ │ (default) │ │ SignatureVerifier │ │ +│ │ │ │ HashVerifier │ │ +│ │ │ │ MetaVerifier │ │ +│ │ │ │ │ │ +│ └─────────────┘ └──────────────────────┘ │ +│ │ +├──────────────────────────────────────────────┤ +│ Core │ +│ │ +│ ContentInfo>│ +│ InAppPurchase, SignerInfo, etc. │ +│ │ +│ PKCS#7 types: ContentInfo, SignedData, │ +│ EncapsulatedContentInfo, SignerInfo, │ +│ AlgorithmIdentifier, Attribute, Version │ +└──────────────────────────────────────────────┘ +``` + +## Core + +`AppReceipt` is a typealias: + +```swift +public typealias AppReceipt = ContentInfo> +``` + +The generic PKCS#7 types (`ContentInfo`, `SignedData`, `EncapsulatedContentInfo`, `SignerInfo`, etc.) model the full ASN.1 structure. `InAppReceiptPayload` and `InAppPurchase` hold the decoded receipt data. + +All types are `Sendable` and `Hashable`. + +## Decoder + +``` +┌──────────────────┐ +│ AppReceiptDecoder│ +│ │ +│ engine: Engine │───▶ Protocol +└──────────────────┘ + │ + ▼ +┌──────────────────────────┐ +│ SwiftASN1ReceiptDecoder │ (default) +│ │ +│ Uses swift-asn1 BER/DER │ +│ parsing + PKCS7+SwiftASN1│ +│ extensions │ +└──────────────────────────┘ +``` + +`AppReceiptDecoder` takes an `Engine` — a protocol with a single `decode(from:)` method. The default engine is `SwiftASN1ReceiptDecoder` which uses Apple's swift-asn1 for BER/DER parsing. + +To use a custom decoder: + +```swift +struct MyEngine: AppReceiptDecoder.Engine { + func decode(from data: Data) throws -> AppReceipt { + // custom parsing + } +} + +let decoder = AppReceiptDecoder(engine: MyEngine()) +let receipt = try decoder.decode(from: data) +``` + +## Validator + +``` +┌──────────────────────┐ +│ ReceiptValidator │ +│ │ +│ @VerifierBuilder │ +│ verifiers: [any │ +│ ReceiptVerifier] │ +└──────────┬───────────┘ + │ + │ TaskGroup (parallel) + │ + ┌─────┼─────┬──────────┐ + ▼ ▼ ▼ ▼ + Chain Sig Hash Meta +``` + +`ReceiptValidator` composes `ReceiptVerifier` conformances via `@VerifierBuilder`. Default validation runs four verifiers in parallel using `TaskGroup`. If any fails, the rest are cancelled. + +### Implementing a Custom Verifier + +```swift +struct MyVerifier: ReceiptVerifier { + func verify(_ receipt: ReceiptValidatable) async -> VerificationResult { + // custom check + return .valid + } +} + +let validator = ReceiptValidator { + X509ChainVerifier(rootCertificates: [rootCert]) + MyVerifier() +} +``` + +### Verifier Variants + +Each verification concern has two implementations: + +| Concern | Async (default) | Blocking (Security.framework) | +|---------|-----------------|-------------------------------| +| Chain | `X509ChainVerifier` | `SecChainVerifier` | +| Signature | `SignatureVerifier` | `SecSignatureVerifier` | +| Hash | `HashVerifier` | `HashVerifier` (same) | +| Meta | `MetaVerifier` | `MetaVerifier` (same) | + +The async variants use swift-certificates and swift-crypto. The `Sec` variants use Apple's Security.framework and are used in the blocking validation path. diff --git a/Sources/Docs.docc/Blocking Mode.md b/Sources/Docs.docc/Blocking Mode.md new file mode 100644 index 0000000..0cce9a1 --- /dev/null +++ b/Sources/Docs.docc/Blocking Mode.md @@ -0,0 +1,18 @@ +# Blocking Mode + +Synchronous API for contexts where async is not available. + +## Usage + +```swift +@_spi(Blocking) import TPInAppReceipt + +let receipt = try AppReceipt.local_blocking +let result = receipt.validate_blocking() +``` + +## How It Differs + +The blocking validator uses Security.framework verifiers (`SecChainVerifier`, `SecSignatureVerifier`) instead of the async swift-certificates/swift-crypto ones. Hash and metadata verification are the same. + +Blocking validation runs verifiers sequentially, not in parallel. diff --git a/Sources/Docs.docc/Receipt Fields.md b/Sources/Docs.docc/Receipt Fields.md new file mode 100644 index 0000000..421ad5c --- /dev/null +++ b/Sources/Docs.docc/Receipt Fields.md @@ -0,0 +1,260 @@ +# Receipt Fields + +ASN.1 field types present in an App Store receipt. + +> Based on [Apple's Receipt Fields Reference](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html). Fields not documented by Apple are marked as reserved/unknown. + +## App Receipt Fields + +### Bundle Identifier + +The app's bundle identifier. Corresponds to the value of `CFBundleIdentifier` in the `Info.plist` file. Use this value to validate if the receipt was indeed generated for your app. + +- ASN.1 Field Type: 2 +- ASN.1 Field Value: UTF8STRING + +### App Version + +The app's version number. Corresponds to the value of `CFBundleVersion` (in iOS) or `CFBundleShortVersionString` (in macOS) in the `Info.plist` file. + +- ASN.1 Field Type: 3 +- ASN.1 Field Value: UTF8STRING + +### Opaque Value + +An opaque value used, with other data, to compute the SHA-1 hash during validation. + +- ASN.1 Field Type: 4 +- ASN.1 Field Value: A series of bytes + +### SHA-1 Hash + +A SHA-1 hash, used to validate the receipt. + +- ASN.1 Field Type: 5 +- ASN.1 Field Value: 20-byte SHA-1 digest + +### Environment + +The receipt's environment. + +- ASN.1 Field Type: 0 +- ASN.1 Field Value: UTF8STRING +- Values: `Production`, `ProductionSandbox`, `Sandbox`, `Xcode` + +> Not documented by Apple in the original reference. Present in receipts from all environments. + +### App Store ID + +The App Store ID. + +- ASN.1 Field Type: 1 +- ASN.1 Field Value: INTEGER + +> Not documented by Apple. Only assigned in production. + +### Transaction Date + +The transaction date. + +- ASN.1 Field Type: 8 +- ASN.1 Field Value: IA5STRING, interpreted as an RFC 3339 date + +> Not documented by Apple. + +### Fulfillment Tool Version + +The fulfillment tool version. + +- ASN.1 Field Type: 9 +- ASN.1 Field Value: INTEGER + +> Not documented by Apple. + +### Age Rating + +The age rating of the app. + +- ASN.1 Field Type: 10 +- ASN.1 Field Value: UTF8STRING + +> Not documented by Apple. + +### Developer ID + +The developer ID. + +- ASN.1 Field Type: 11 +- ASN.1 Field Value: INTEGER + +> Not documented by Apple. + +### Receipt Creation Date + +The date when the app receipt was created. When validating the receipt, use this date to validate the receipt's signature. + +- ASN.1 Field Type: 12 +- ASN.1 Field Value: IA5STRING, interpreted as an RFC 3339 date + +### Download ID + +The download ID. + +- ASN.1 Field Type: 15 +- ASN.1 Field Value: INTEGER + +> Not documented by Apple. + +### Installer Version ID + +The installer version ID. + +- ASN.1 Field Type: 16 +- ASN.1 Field Value: INTEGER + +> Not documented by Apple. + +### In-App Purchase Receipt + +The receipt for an in-app purchase. In the ASN.1 file, there are multiple fields that all have type 17, each of which contains a single in-app purchase receipt. + +- ASN.1 Field Type: 17 +- ASN.1 Field Value: SET of in-app purchase receipt attributes + +An empty array is a valid receipt. + +### Original Purchase Date + +The date when the app was originally purchased. + +- ASN.1 Field Type: 18 +- ASN.1 Field Value: IA5STRING, interpreted as an RFC 3339 date + +### Original Application Version + +The version of the app that was originally purchased. Corresponds to the value of `CFBundleVersion` (in iOS) or `CFBundleShortVersionString` (in macOS). In the sandbox environment, the value is always `"1.0"`. + +- ASN.1 Field Type: 19 +- ASN.1 Field Value: UTF8STRING + +### Receipt Expiration Date + +The date that the app receipt expires. Only present for apps purchased through the Volume Purchase Program. If this key is not present, the receipt does not expire. + +- ASN.1 Field Type: 21 +- ASN.1 Field Value: IA5STRING, interpreted as an RFC 3339 date + +### Reserved Fields + +The following field type IDs are present in receipts but not documented by Apple: + +| ASN.1 Field Type | Status | +|-------------------|--------| +| 6 | Reserved | +| 7 | Reserved | +| 13 | Reserved | +| 14 | Reserved | +| 20 | Reserved | +| 25 | Reserved | + +--- + +## In-App Purchase Receipt Fields + +### Quantity + +The number of items purchased. Corresponds to the `quantity` property of the `SKPayment` object stored in the transaction's `payment` property. + +- ASN.1 Field Type: 1701 +- ASN.1 Field Value: INTEGER + +### Product Identifier + +The product identifier of the item that was purchased. Corresponds to the `productIdentifier` property of the `SKPayment` object stored in the transaction's `payment` property. + +- ASN.1 Field Type: 1702 +- ASN.1 Field Value: UTF8STRING + +### Transaction Identifier + +The transaction identifier of the item that was purchased. Corresponds to the transaction's `transactionIdentifier` property. + +- ASN.1 Field Type: 1703 +- ASN.1 Field Value: UTF8STRING + +### Purchase Date + +The date and time that the item was purchased. Corresponds to the transaction's `transactionDate` property. For auto-renewable subscriptions, this is either the purchase date or the renewal date. + +- ASN.1 Field Type: 1704 +- ASN.1 Field Value: IA5STRING, interpreted as an RFC 3339 date + +### Original Transaction Identifier + +For a transaction that restores a previous transaction, the transaction identifier of the original transaction. Otherwise, identical to the transaction identifier. All receipts in a chain of renewals for an auto-renewable subscription have the same value. + +- ASN.1 Field Type: 1705 +- ASN.1 Field Value: UTF8STRING + +### Original Purchase Date + +For a transaction that restores a previous transaction, the date of the original transaction. For auto-renewable subscriptions, this indicates the beginning of the subscription period, even if the subscription has been renewed. + +- ASN.1 Field Type: 1706 +- ASN.1 Field Value: IA5STRING, interpreted as an RFC 3339 date + +### Product Type + +The type of in-app product. + +- ASN.1 Field Type: 1707 +- ASN.1 Field Value: INTEGER +- Values: 0 (non-consumable), 1 (consumable), 2 (non-renewing subscription), 3 (auto-renewable subscription) + +### Subscription Expiration Date + +The expiration date for the subscription. Only present for auto-renewable subscription receipts. + +- ASN.1 Field Type: 1708 +- ASN.1 Field Value: IA5STRING, interpreted as an RFC 3339 date + +### Web Order Line Item ID + +The primary key for identifying subscription purchases. This value is a unique ID that identifies purchase events across devices, including subscription renewal purchase events. + +- ASN.1 Field Type: 1711 +- ASN.1 Field Value: INTEGER + +### Cancellation Date + +For a transaction that was canceled, the time and date of the cancellation. For an auto-renewable subscription plan that was upgraded, the time and date of the upgrade transaction. + +- ASN.1 Field Type: 1712 +- ASN.1 Field Value: IA5STRING, interpreted as an RFC 3339 date + +Treat a canceled receipt the same as if no purchase had ever been made. + +### Subscription Trial Period + +Whether the subscription is in the free trial period. + +- ASN.1 Field Type: 1713 +- ASN.1 Field Value: INTEGER +- Values: 1 (true), 0 (false) + +### Subscription Introductory Price Period + +Whether an auto-renewable subscription is in the introductory price period. + +- ASN.1 Field Type: 1719 +- ASN.1 Field Value: INTEGER +- Values: 1 (true), 0 (false) + +If a previous subscription period in the receipt has a value of `true` for either the trial period or introductory price period, the user is not eligible for a free trial or introductory price within that subscription group. + +### Promotional Offer Identifier + +The identifier of the subscription offer redeemed by the user. + +- ASN.1 Field Type: 1721 +- ASN.1 Field Value: UTF8STRING diff --git a/Sources/Docs.docc/TPInAppReceipt 4.0 Migration Guide.md b/Sources/Docs.docc/TPInAppReceipt 4.0 Migration Guide.md new file mode 100644 index 0000000..f11f2e8 --- /dev/null +++ b/Sources/Docs.docc/TPInAppReceipt 4.0 Migration Guide.md @@ -0,0 +1,120 @@ +# Migrating from v3 to v4 + +What changed and how to update your code. + +## Dependencies + +```swift +.package(url: "https://github.com/tikhop/TPInAppReceipt.git", from: "4.0.0") +``` + +## Platform Requirements + +- iOS 12 → iOS 13 +- macOS 10.13 → macOS 10.15 +- tvOS 12 → tvOS 13 +- Swift 5.9 → Swift 6.0 + +## Type Changes + +```swift +// Before +let receipt = try InAppReceipt.localReceipt() +// or +let receipt = try InAppReceipt() + +// After +let receipt = try await AppReceipt.local +``` + +```swift +// Before +let receipt = try InAppReceipt.receipt(from: data) + +// After +let receipt = try AppReceipt.receipt(from: data) +``` + +## Validation + +```swift +// Before +do { + try receipt.verify() +} catch IARError.validationFailed(reason: .hashValidation) { + // ... +} + +// After +let result = await receipt.validate() +switch result { +case .valid: + break +case .invalid(let error): + // error is typed: ReceiptValidatorError, ChainVerificationError, etc. + print(error) +} +``` + +Individual verification methods (`verifyHash()`, `verifySignature()`, `verifyBundleIdentifier()`) are removed. Use custom `ReceiptValidator` composition instead. See . + +## Error Types + +| v3 | v4 | +|----|-----| +| `IARError` | `AppReceiptError` | +| `IARError.validationFailed(reason:)` | `ReceiptValidatorError`, `ChainVerificationError`, `HashVerificationError`, etc. | + +## Property Changes + +- `originalAppVersion` is now `String?` (was `String`) +- `ageRating` is now `String?` (was `String`) +- `environment` is now `InAppReceiptPayload.Environment` enum (was `String`). Update comparisons: `receipt.environment == .production` instead of `receipt.environment == "Production"`. +- `signature` is now `Data` (was `Data?`) +- `InAppPurchase` properties are now `let` (was `var`) +- `InAppPurchase.originalPurchaseDate` is now `Date?` (was `Date!`) +- `autoRenewablePurchases` — same API, works on `AppReceipt` +- `activeAutoRenewableSubscriptionPurchases` — same API + +## Blocking API + +If you prefer synchronous api, import the library with `@_spi(Blocking)` to access blocking variants of the async API. + +```swift +@_spi(Blocking) import TPInAppReceipt + +// Load receipt +let receipt = try AppReceipt.local_blocking + +// Validate +let result = receipt.validate_blocking() +``` + +See for more details. + +## Receipt Refresh + +```swift +// Before +InAppReceipt.refresh { error in + // ... +} + +// After +try await AppReceipt.refresh() +``` + +`cancelRefreshSession()` and `isReceiptRefreshingNow` are removed. Use Swift Concurrency task cancellation instead. + +## Receipt Fields + +`InAppReceiptField` has been renamed to `AppReceiptField` and changed from a struct with static constants to an enum. Purchase-specific fields are now in a separate `InAppPurchaseReceiptField` enum. + +## Removed + +- Objective-C bindings +- CocoaPods support +- `isValid` computed property — use `await receipt.validate()` instead +- `base64` computed property +- `SKSubscriptionGroup` class and related `SKProductsResponse` extensions +- `isEligibleForIntroductoryOffer(for: SKSubscriptionGroup)` overload — use `Set` variant instead diff --git a/Sources/Docs.docc/Validating Receipt.md b/Sources/Docs.docc/Validating Receipt.md new file mode 100644 index 0000000..1104fbf --- /dev/null +++ b/Sources/Docs.docc/Validating Receipt.md @@ -0,0 +1,63 @@ +# Validating a Receipt + +Verify the authenticity and integrity of an App Store receipt. + +## Default Validation + +Runs four checks in parallel: certificate chain, signature, hash, and metadata. + +```swift +let result = await receipt.validate() + +switch result { +case .valid: + break +case .invalid(let error): + print(error) +} +``` + +The validator auto-selects the Apple root certificate based on the receipt's environment. + +## Custom Validators + +Build your own with `@VerifierBuilder`: + +```swift +let validator = ReceiptValidator { + X509ChainVerifier(rootCertificates: [rootCert]) + SignatureVerifier() + HashVerifier(deviceIdentifier: deviceId) + MetaVerifier( + appVersionProvider: { Bundle.main.appVersion }, + bundleIdentifierProvider: { Bundle.main.bundleIdentifier } + ) +} + +let result = await validator.validate(receipt) +``` + +Include or exclude any verifier. They run in parallel via `TaskGroup` — if any fails, the rest are cancelled. + +## Built-in Verifiers + +| Verifier | What it checks | +|----------|---------------| +| `X509ChainVerifier` | Certificate chain against Apple root (swift-certificates) | +| `SecChainVerifier` | Certificate chain via Security.framework | +| `SignatureVerifier` | PKCS#7 signature (swift-crypto) | +| `SecSignatureVerifier` | PKCS#7 signature via Security.framework | +| `HashVerifier` | SHA-1 hash: device ID + opaque value + bundle ID | +| `MetaVerifier` | Bundle identifier and app version match | + +`X509`/`Signature` variants are async. `Sec` variants use Security.framework and work in blocking contexts. + +## Error Types + +- ``ReceiptValidatorError`` — invalid structure, missing root certificate, missing device identifier +- ``ChainVerificationError`` — certificate data invalid, chain validation failed, revocation check failed +- ``SignatureVerificationError`` — invalid key, invalid signature +- ``HashVerificationError`` — missing device identifier, hash mismatch +- ``MetaVerificationError`` — bundle identifier mismatch, version mismatch + +See for how the validation layer fits into the overall design. diff --git a/Sources/Docs.docc/Working with Receipt.md b/Sources/Docs.docc/Working with Receipt.md new file mode 100644 index 0000000..5149b8c --- /dev/null +++ b/Sources/Docs.docc/Working with Receipt.md @@ -0,0 +1,73 @@ +# Working with a Receipt + +Decode and read App Store receipt data. + +## Decoding + +```swift +import TPInAppReceipt + +// Local receipt +let receipt = try await AppReceipt.local + +// From raw data +let receipt = try AppReceipt.receipt(from: data) +``` + +`AppReceipt` is a typealias over `ContentInfo>` — the full PKCS#7 structure is accessible. + +## Reading + +```swift +receipt.bundleIdentifier +receipt.appVersion +receipt.originalAppVersion // String? +receipt.environment // .production, .sandbox, .xcode, .productionSandbox +receipt.creationDate +receipt.expirationDate // VPP only +``` + +See for the full list of fields. + +## Purchases + +```swift +receipt.purchases // [InAppPurchase] +receipt.hasPurchases +receipt.autoRenewablePurchases +receipt.activeAutoRenewableSubscriptionPurchases +``` + +Query by product: + +```swift +receipt.purchases(ofProductIdentifier: "com.example.sub") +receipt.containsPurchase(ofProductIdentifier: "com.example.premium") + +receipt.hasActiveAutoRenewableSubscription( + ofProductIdentifier: "com.example.sub", + forDate: Date() +) + +receipt.lastAutoRenewableSubscriptionPurchase( + ofProductIdentifier: "com.example.sub" +) +``` + +## Introductory Offer Eligibility + +Pass product identifiers within the same subscription group: + +```swift +receipt.isEligibleForIntroductoryOffer( + for: ["com.example.bronze", "com.example.silver", "com.example.gold"] +) +``` + +## Refreshing + +Request an updated receipt from the App Store: + +```swift +try await AppReceipt.refresh() +``` diff --git a/Sources/Error.swift b/Sources/Error.swift new file mode 100644 index 0000000..126cb06 --- /dev/null +++ b/Sources/Error.swift @@ -0,0 +1,24 @@ +/// Errors that can occur when working with app receipts. +public enum AppReceiptError: Error, Sendable { + /// The app store receipt was not found on the device. + /// + /// This typically occurs when trying to access the local receipt for an app + /// that hasn't been purchased from the App Store. + case appStoreReceiptNotFound + + /// The receipt content is invalid or corrupted. + /// + /// - Parameter error: The underlying error that caused the content to be invalid. + case receiptContentInvalid(any Error) + + /// The receipt payload is missing or invalid. + /// + /// This indicates the receipt structure doesn't contain the expected payload data. + case receiptPayloadMissingOrInvalid + + /// Receipt decoding failed. + /// + /// This occurs when the receipt data cannot be decoded from its ASN.1 format. + /// - Parameter error: The underlying error that caused the decoding failure. + case decodingFailed(any Error) +} diff --git a/Sources/Extensions/Bundle+Extension.swift b/Sources/Extensions/Bundle+Extension.swift new file mode 100644 index 0000000..1dd1327 --- /dev/null +++ b/Sources/Extensions/Bundle+Extension.swift @@ -0,0 +1,50 @@ +import Foundation + +extension Bundle { + /// Appropriate app version for receipt validation + var appVersion: String? { + #if targetEnvironment(macCatalyst) || os(macOS) + let dictKey = "CFBundleShortVersionString" + #else + let dictKey = "CFBundleVersion" + #endif + + return infoDictionary?[dictKey] as? String + } + + /// Retrieve local App Store Receipt Data + /// + /// - Returns: 'Data' object that represents local receipt + /// - throws: An error if receipt file not found or 'Data' can't be created + func appStoreReceiptData() async -> Data? { + guard let receiptUrl = appStoreReceiptURL else { return nil } + return try? await Data.readBytes(from: receiptUrl) + } + + /// Retrieve local App Store Receipt Data in base64 string + /// + /// - Returns: 'Data' object that represents local receipt + /// - throws: An error if receipt file not found or 'Data' can't be created + public func appStoreReceiptBase64() async -> String? { + await appStoreReceiptData()?.base64EncodedString() + } + + static func appleRootCertificateData(testing: Bool) async -> Data? { + let filename = testing ? "StoreKitTestCertificate.cer" : "AppleIncRootCertificate.cer" + guard let url = url(for: filename) else { + return nil + } + + return try? await Data.readBytes(from: url) + } + + static func url(for file: String) -> URL? { + for bundle in [Bundle.module, Bundle.main] { + if let url = bundle.url(forResource: file, withExtension: "") { + return url + } + } + + return nil + } +} diff --git a/Sources/Extensions/Data+Extension.swift b/Sources/Extensions/Data+Extension.swift new file mode 100644 index 0000000..5aec3d6 --- /dev/null +++ b/Sources/Extensions/Data+Extension.swift @@ -0,0 +1,11 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +extension Data { + static func readBytes(from url: URL) async throws -> Data { + try await URLSession.shared.data(from: url).0 + } +} diff --git a/Sources/Extensions/Date+Extension.swift b/Sources/Extensions/Date+Extension.swift new file mode 100644 index 0000000..ea53374 --- /dev/null +++ b/Sources/Extensions/Date+Extension.swift @@ -0,0 +1,18 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +extension Date { + static func date(from rfc3339String: String) -> Date? { + rfc3339DateFormatter.date(from: rfc3339String) + } +} + +private nonisolated(unsafe) let rfc3339DateFormatter: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = .withInternetDateTime + formatter.timeZone = TimeZone(abbreviation: "UTC") + return formatter +}() diff --git a/Sources/Extensions/UUID+Extension.swift b/Sources/Extensions/UUID+Extension.swift new file mode 100644 index 0000000..f1c2487 --- /dev/null +++ b/Sources/Extensions/UUID+Extension.swift @@ -0,0 +1,13 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +extension UUID { + @inlinable + var data: Data { + var uuidBytes = uuid + return Data(bytes: &uuidBytes, count: MemoryLayout.size(ofValue: uuidBytes)) + } +} diff --git a/Sources/Extras.swift b/Sources/Extras.swift index d68e61a..e1aa2fc 100644 --- a/Sources/Extras.swift +++ b/Sources/Extras.swift @@ -1,262 +1,289 @@ -// -// Extras.swift -// TPInAppReceipt -// -// Created by Pavel Tikhonenko on 13.02.2020. -// Copyright © 2021 Pavel Tikhonenko. All rights reserved. -// +extension AppReceipt { + /// Retrieves the original transaction identifier for a product. + /// + /// Gets the original transaction ID of the first purchase for the specified product identifier. + /// + /// - Parameter productIdentifier: The product identifier to search for. + /// - Returns: The original transaction identifier, or nil if no purchase exists for the product. + public func originalTransactionIdentifier(ofProductIdentifier productIdentifier: String) -> String? { + purchases(ofProductIdentifier: productIdentifier) + .first? + .originalTransactionIdentifier + } -#if canImport(StoreKit) + /// Checks whether the receipt contains a purchase for a specific product. + /// + /// Determines if the receipt includes any purchase record for the specified product identifier. + /// + /// - Parameter productIdentifier: The product identifier to search for. + /// - Returns: `true` if a purchase exists for the product, `false` otherwise. + public func containsPurchase(ofProductIdentifier productIdentifier: String) -> Bool { + for item in purchases { + if item.productIdentifier == productIdentifier { + return true + } + } -import Foundation -import StoreKit - -@available(watchOSApplicationExtension 6.2, *) -fileprivate var refreshSession: RefreshSession? - -@available(tvOS 12.0, *) -@available(macOS 10.14, *) -@available(iOS 12.0, *) -public class SKSubscriptionGroup -{ - let identifier: GroupIdentifier - var products: Set = [] - - init(with identifier: GroupIdentifier) - { - self.identifier = identifier - self.products = [] - } - - init(with products: Set) - { - guard let gid = products.first?.subscriptionGroupIdentifier else - { - fatalError("All products must have subscriptionGroupIdentifier") - } - - self.identifier = gid - self.products = products - } - - func insert(product: SKProduct) - { - if identifier != product.subscriptionGroupIdentifier - { - fatalError("`Product.subscriptionGroupIdentifier` must be equal to current identifier") - } - - products.insert(product) - } - - func contains(_ productIdentifier: String) -> Bool - { - return products.contains(where: { $0.productIdentifier == productIdentifier }) - } + return false + } + + /// Retrieves all purchases for a specific product identifier. + /// + /// Finds all ``InAppPurchase`` records matching the specified product identifier. + /// By default, results are sorted by purchase date in descending order (most recent first). + /// + /// - Parameters: + /// - productIdentifier: The product identifier to search for. + /// - sort: An optional custom sorting function. If not provided, purchases are sorted by date descending. + /// - Returns: An array of ``InAppPurchase`` objects for the product, empty if none exist. + public func purchases( + ofProductIdentifier productIdentifier: String, + sortedBy sort: ((InAppPurchase, InAppPurchase) -> Bool)? = nil + ) -> [InAppPurchase] { + let filtered: [InAppPurchase] = purchases.filter { + $0.productIdentifier == productIdentifier + } + + guard let sort = sort else { + return filtered.sorted { $0.purchaseDate > $1.purchaseDate } + } + return filtered.sorted { sort($0, $1) } + } + + /// Retrieves an active auto-renewable subscription purchase for a product at a specific date. + /// + /// Finds the first active ``InAppPurchase`` for the specified product identifier + /// that is active on the given date. + /// + /// - Parameters: + /// - productIdentifier: The product identifier to search for. + /// - date: The date to check subscription activity against. + /// - Returns: An active ``InAppPurchase`` if one exists, nil otherwise. + public func activeAutoRenewableSubscriptionPurchases( + ofProductIdentifier productIdentifier: String, + forDate date: Date + ) -> InAppPurchase? { + let filtered = purchases(ofProductIdentifier: productIdentifier) + + for purchase in filtered { + if purchase.isActiveAutoRenewableSubscription(forDate: date) { + return purchase + } + } + + return nil + } + + /// Retrieves the most recent auto-renewable subscription purchase for a product. + /// + /// Finds the ``InAppPurchase`` for the specified product identifier with the latest + /// subscription expiration date. + /// + /// - Parameter productIdentifier: The product identifier to search for. + /// - Returns: The most recent ``InAppPurchase`` if one exists, nil otherwise. + public func lastAutoRenewableSubscriptionPurchase( + ofProductIdentifier productIdentifier: String + ) -> InAppPurchase? { + var purchase: InAppPurchase? = nil + let filtered = purchases(ofProductIdentifier: productIdentifier) + + var lastInterval: TimeInterval = 0 + for iap in filtered { + if let thisInterval = iap.subscriptionExpirationDate?.timeIntervalSince1970 { + if purchase == nil || thisInterval > lastInterval { + purchase = iap + lastInterval = thisInterval + } + } + } + + return purchase + } + + /// Checks whether there is an active subscription for a product at a specific date. + /// + /// Determines if the receipt contains an active ``InAppPurchase`` for the specified product identifier + /// that is active on the given date. + /// + /// - Parameters: + /// - productIdentifier: The product identifier to search for. + /// - date: The date to check subscription activity against. + /// - Returns: `true` if an active subscription exists, `false` otherwise. + public func hasActiveAutoRenewableSubscription( + ofProductIdentifier productIdentifier: String, + forDate date: Date + ) -> Bool { + activeAutoRenewableSubscriptionPurchases( + ofProductIdentifier: productIdentifier, + forDate: date + ) != nil + } + + /// Returns whether the customer is eligible for an introductory offer + /// within the provided subscription group. + /// + /// Returns `true` if the customer has never redeemed an introductory offer + /// or free trial for any product in the subscription group. + /// + /// - Parameter group: A set of product identifiers in the subscription group. + /// - Returns: `true` if the customer is eligible for an introductory offer + /// on any auto-renewable subscription within the subscription group; `false` otherwise. + public func isEligibleForIntroOffer(for group: Set) -> Bool { + payload.isEligibleForIntroOffer(for: group) + } + + /// Returns whether the customer is eligible for an introductory offer + /// for the specified product. + /// + /// Returns `true` if the customer has never redeemed an introductory offer + /// or free trial for this product. + /// + /// - Parameter productIdentifier: The product identifier to check eligibility for. + /// - Returns: `true` if the customer is eligible for an introductory offer; `false` otherwise. + public func isEligibleForIntroOffer(for productIdentifier: String) -> Bool { + payload.isEligibleForIntroOffer(for: productIdentifier) + } } -public typealias GroupIdentifier = String - -@available(macOS 10.14, *) -public extension SKProductsResponse -{ - /// Build a `SKSubscriptionGroup` object - /// - /// We assume that all retrieved products `(SKProduct)` belong to the same subscription group - /// - /// - Returns `SKSubscriptionGroup`. Empty if no subscription groups found - var subscriptionGroup: SKSubscriptionGroup - { - var group: SKSubscriptionGroup! - - for p in products - { - guard let pgid = p.subscriptionGroupIdentifier else - { - continue - } - - if group == nil - { - group = SKSubscriptionGroup(with: pgid) - } - - group.insert(product: p) - } - - guard let g = group else - { - fatalError("`group` can't be nil.") - } - - return g - } - - /// Build a dictionary that contains the subscription groups - /// - /// The dictionary contains `SKSubscriptionGroup` objects where the key is a group identifier `GroupIdentifier` - /// - /// - Returns `[GroupIdentifier: SKSubscriptionGroup]`. Empty if no subscription groups found - var subscriptionGroups: [GroupIdentifier: SKSubscriptionGroup] - { - var groups: [GroupIdentifier: SKSubscriptionGroup] = [:] - - for p in products - { - guard let gid = p.subscriptionGroupIdentifier else - { - continue - } - - guard let group = groups[gid] else - { - let g = SKSubscriptionGroup(with: gid) - g.insert(product: p) - - groups[gid] = g - - continue - } - - group.insert(product: p) - } - - return groups - } +// MARK: - InAppPurchase Refund Status + +extension InAppPurchase { + /// A Boolean value indicating whether the purchase has been canceled or refunded. + /// + /// Applies to auto-renewable subscriptions, non-consumable products, and non-renewing subscriptions. + /// Returns `true` when Apple customer support has canceled the transaction or the customer received a refund. + /// + /// - Note: For auto-renewable subscriptions, this also returns `true` when the subscription + /// was upgraded to a different product. The local receipt does not include a cancellation reason + /// to distinguish refunds from upgrades. + public var isRefunded: Bool { + cancellationDate != nil + } } -@available(macOS 10.14, *) -public extension InAppReceipt -{ - - /// Refresh local in-app receipt - /// - /// - Parameter completion: handler for result - @available(watchOSApplicationExtension 6.2, *) - static func refresh(completion: @escaping IAPRefreshRequestResult) - { - if refreshSession != nil - { - completion(IARError.receiptRefreshingInProgress) - return - } - - refreshSession = RefreshSession() - refreshSession!.refresh { (error) in - completion(error) - InAppReceipt.destroyRefreshSession() +// MARK: - InAppReceiptPayload Intro Offer Eligibility + +extension InAppReceiptPayload { + /// Returns whether the customer is eligible for an introductory offer + /// within the provided subscription group. + /// + /// Returns `true` if the customer has never redeemed an introductory offer + /// or free trial for any product in the subscription group. + /// + /// - Parameter group: A set of product identifiers in the subscription group. + /// - Returns: `true` if the customer is eligible for an introductory offer + /// on any auto-renewable subscription within the subscription group; `false` otherwise. + public func isEligibleForIntroOffer(for group: Set) -> Bool { + !purchases.contains { + group.contains($0.productIdentifier) + && ($0.subscriptionTrialPeriod || $0.subscriptionIntroductoryPricePeriod) } } - /// Cancel refreshing local in-app receipt - @available(watchOSApplicationExtension 6.2, *) - static func cancelRefreshSession() - { - refreshSession?.cancel() - destroyRefreshSession() - } - - @available(watchOSApplicationExtension 6.2, *) - static fileprivate func destroyRefreshSession() - { - refreshSession = nil + /// Returns whether the customer is eligible for an introductory offer + /// for the specified product. + /// + /// Returns `true` if the customer has never redeemed an introductory offer + /// or free trial for this product. + /// + /// - Parameter productIdentifier: The product identifier to check eligibility for. + /// - Returns: `true` if the customer is eligible for an introductory offer; `false` otherwise. + public func isEligibleForIntroOffer(for productIdentifier: String) -> Bool { + !purchases.contains { + $0.productIdentifier == productIdentifier + && ($0.subscriptionTrialPeriod || $0.subscriptionIntroductoryPricePeriod) + } } - - /// Check whether receipt is refreshing now - /// - /// - Returns `true` if receipt is refreshing now, otherwise `false` - @available(watchOSApplicationExtension 6.2, *) - static var isReceiptRefreshingNow: Bool - { - refreshSession != nil - } - - /// Check whether user is eligible for introductory offer for any products within the same subscription group - /// - /// - Returns `false` if user isn't eligible for introductory offer, otherwise `true` - func isEligibleForIntroductoryOffer(for group: SKSubscriptionGroup) -> Bool - { - let array = purchases.filter { $0.subscriptionTrialPeriod || $0.subscriptionIntroductoryPricePeriod } - .filter { group.contains($0.productIdentifier) } - - return array.isEmpty - } } -public typealias IAPRefreshRequestResult = ((Error?) -> ()) +#if canImport(StoreKit) -@available(watchOSApplicationExtension 6.2, *) -fileprivate class RefreshSession : NSObject, SKRequestDelegate -{ - private let receiptRefreshRequest = SKReceiptRefreshRequest() - private var completion: IAPRefreshRequestResult? - - override init() - { - super.init() +import Foundation +@preconcurrency import StoreKit - receiptRefreshRequest.delegate = self +extension AppReceipt { + /// Requests a refresh of the app receipt from the App Store. + /// + /// Initiates an asynchronous receipt refresh request using StoreKit. + /// This is useful when you need to update the local receipt with new purchase information. + /// + /// If a refresh is already in progress, subsequent calls will await the existing + /// request rather than starting a new one. + /// + /// - Throws: An error if the receipt refresh request fails. + public static func refresh() async throws { + try await RefreshCoordinator.shared.refresh() } - - func refresh(completion: @escaping IAPRefreshRequestResult) - { - self.completion = completion - - receiptRefreshRequest.start() +} + +private final class RefreshCoordinator: @unchecked Sendable { + static let shared = RefreshCoordinator() + + private let lock = NSLock() + private var ongoingTask: Task? + + func refresh() async throws { + let task: Task = lock.withLock { + if let existing = ongoingTask { + return existing + } + let task = Task { + defer { + self.lock.withLock { self.ongoingTask = nil } + } + try await Self.performRefresh() + } + ongoingTask = task + return task + } + try await task.value } - - func requestDidFinish(_ request: SKRequest) - { - requestDidFinish(with: nil) - receiptRefreshRequest.cancel() + + private static func performRefresh() async throws { + let request = SKReceiptRefreshRequest() + let delegate = RefreshDelegate() + + request.delegate = delegate + + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + delegate.setContinuation(continuation) + request.start() + } } - - func request(_ request: SKRequest, didFailWithError error: Error) - { - print("Something went wrong: \(error.localizedDescription)") - - requestDidFinish(with: error) +} + +private final class RefreshDelegate: NSObject, SKRequestDelegate, @unchecked Sendable { + private let lock = NSLock() + private var continuation: CheckedContinuation? + + func setContinuation(_ continuation: CheckedContinuation) { + lock.withLock { + self.continuation = continuation + } } - - func requestDidFinish(with error: Error?) - { - DispatchQueue.main.async { [weak self] in - self?.completion?(error) + + func requestDidFinish(_: SKRequest) { + resume(with: .success(())) + } + + func request(_: SKRequest, didFailWithError error: Error) { + resume(with: .failure(error)) + } + + private func resume(with result: Result) { + let cont: CheckedContinuation? = lock.withLock { + let c = continuation + continuation = nil + return c + } + + switch result { + case .success: + cont?.resume() + case let .failure(error): + cont?.resume(throwing: error) } - receiptRefreshRequest.cancel() } - - func cancel() - { - receiptRefreshRequest.cancel() - } } #endif - -public typealias SubscriptionGroup = Set - -public extension InAppReceipt -{ - /// Check whether user is eligible for introductory offer for any products within the same subscription group - /// - /// - Returns `false` if user isn't eligible for introductory offer, otherwise `true` - func isEligibleForIntroductoryOffer(for group: SubscriptionGroup) -> Bool - { - let array = purchases.filter { $0.subscriptionTrialPeriod || $0.subscriptionIntroductoryPricePeriod } - .filter { group.contains($0.productIdentifier) } - - return array.isEmpty - } - - /// Check whether user is eligible for introductory offer for a specific product - /// - /// - Returns `false` if user isn't eligible for introductory offer, otherwise `true` - func isEligibleForIntroductoryOffer(for productIdentifier: String) -> Bool - { - let array = purchases.filter { $0.subscriptionTrialPeriod || $0.subscriptionIntroductoryPricePeriod } - .filter { $0.productIdentifier == productIdentifier } - - return array.isEmpty - } -} diff --git a/Sources/IARError.swift b/Sources/IARError.swift deleted file mode 100644 index 5722869..0000000 --- a/Sources/IARError.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// IARError.swift -// TPInAppReceipt -// -// Created by Pavel Tikhonenko on 20/01/17. -// Copyright © 2017-2021 Pavel Tikhonenko. All rights reserved. -// - -import Foundation - -/// `IARError` is the error type returned by InAppReceipt. -/// It encompasses a few different types of errors, each with their own associated reasons. -/// -/// - initializationFailed: Error occurs during initialization step -/// - validationFailed: Error occurs during the receipt validation process -public enum IARError: Error -{ - case initializationFailed(reason: ReceiptInitializationFailureReason) - case validationFailed(reason: ValidationFailureReason) - case purchaseExpired - case receiptRefreshingInProgress - /// The underlying reason the receipt initialization error occurred. - /// - /// - appStoreReceiptNotFound: In-App Receipt not found - /// - pkcs7ParsingError: PKCS7 Container can't be extracted from in-app receipt data - public enum ReceiptInitializationFailureReason - { - case appStoreReceiptNotFound - case pkcs7ParsingError - case dataIsInvalid - } - - /// The underlying reason the receipt validation error occurred. - /// - /// - hashValidation: Computed hash doesn't match the hash from the receipt's payload - /// - signatureValidation: Error occurs during signature validation. It has several reasons to failure - public enum ValidationFailureReason - { - case hashValidation - case signatureValidation(SignatureValidationFailureReason) - case bundleIdentifierVerification - case bundleVersionVerification - } - - /// The underlying reason the signature validation error occurred. - /// - /// - appleIncRootCertificateNotFound: Apple Inc Root Certificate Not Found - /// - unableToLoadAppleIncRootCertificate: Unable To Load Apple Inc Root Certificate - /// - receiptIsNotSigned: The receipt doesn't contain a signature - /// - receiptSignedDataNotFound: The receipt does contain somr signature, but there is an error while creating a signature object - /// - invalidSignature: The receipt contains invalid signature - public enum SignatureValidationFailureReason - { - case appleIncRootCertificateNotFound - case unableToLoadAppleIncRootCertificate - case unableToLoadAppleIncPublicKey - case unableToLoadiTunesCertificate - case unableToLoadiTunesPublicKey - case unableToLoadWorldwideDeveloperCertificate - case unableToLoadAppleIncPublicSecKey - case receiptIsNotSigned - case receiptSignedDataNotFound - case receiptDataNotFound - case signatureNotFound - case invalidSignature - case invalidCertificateChainOfTrust - } -} diff --git a/Sources/InAppPurchase.swift b/Sources/InAppPurchase.swift deleted file mode 100644 index 27cd91d..0000000 --- a/Sources/InAppPurchase.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// InAppPurchase.swift -// TPInAppReceipt -// -// Created by Pavel Tikhonenko on 19/01/17. -// Copyright © 2017-2021 Pavel Tikhonenko. All rights reserved. -// - -import Foundation - -public struct InAppPurchase -{ - public enum `Type`: Int32 - { - /// Type that we can't recognize for some reason - case unknown = -1 - - /// Type that customers purchase once. They don't expire. - case nonConsumable - - /// Type that are depleted after one use. Customers can purchase them multiple times. - case consumable - - /// Type that customers purchase once and that renew automatically on a recurring basis until customers decide to cancel. - case nonRenewingSubscription - - /// Type that customers purchase and it provides access over a limited duration and don't renew automatically. Customers can purchase them again. - case autoRenewableSubscription - } - - /// The product identifier which purchase related to - public var productIdentifier: String - - /// Product type - public var productType: Type = .unknown - - /// Transaction identifier - public var transactionIdentifier: String - - /// Original Transaction identifier - public var originalTransactionIdentifier: String - - /// Purchase Date - public var purchaseDate: Date - - /// Original Purchase Date. Returns `nil` when testing with StoreKitTest - public var originalPurchaseDate: Date! = nil - - /// Subscription Expiration Date. Returns `nil` if the purchase has been expired (in some cases) - public var subscriptionExpirationDate: Date? = nil - - /// Cancellation Date. Returns `nil` if the purchase is not a renewable subscription - public var cancellationDate: Date? = nil - - /// This value is `true`if the customer’s subscription is currently in the free trial period, or `false` if not. - public var subscriptionTrialPeriod: Bool = false - - /// This value is `true` if the customer’s subscription is currently in an introductory price period, or `false` if not. - public var subscriptionIntroductoryPricePeriod: Bool = false - - /// A unique identifier for purchase events across devices, including subscription-renewal events. This value is the primary key for identifying subscription purchases. - public var webOrderLineItemID: Int? = nil - - /// The value is an identifier of the subscription offer that the user redeemed. - /// Returns `nil` if the user didn't use any subscription offers. - public var promotionalOfferIdentifier: String? = nil - - /// The number of consumable products purchased - /// The default value is `1` unless modified with a mutable payment. The maximum value is 10. - public var quantity: Int = 1 -} - -public extension InAppPurchase -{ - /// A Boolean value indicating whether the purchase is renewable subscription. - var isRenewableSubscription: Bool - { - return subscriptionExpirationDate != nil - } - - /// Check whether the subscription is active for a specific date - /// - /// - Parameter date: The date in which the auto-renewable subscription should be active. - /// - Returns: true if the latest auto-renewable subscription is active for the given date, false otherwise. - func isActiveAutoRenewableSubscription(forDate date: Date) -> Bool - { - assert(isRenewableSubscription, "\(productIdentifier) is not an auto-renewable subscription.") - - if cancellationDate != nil - { - return false - } - - guard let expirationDate = subscriptionExpirationDate else - { - return false - } - - return date >= purchaseDate && date < expirationDate - } -} diff --git a/Sources/InAppReceipt+ASN1Decodable.swift b/Sources/InAppReceipt+ASN1Decodable.swift deleted file mode 100644 index d1ea8f2..0000000 --- a/Sources/InAppReceipt+ASN1Decodable.swift +++ /dev/null @@ -1,334 +0,0 @@ -// -// InAppReceipt+ASN1Decodable.swift -// TPInAppReceipt -// -// Created by Pavel Tikhonenko on 04/08/20. -// Copyright © 2017-2021 Pavel Tikhonenko. All rights reserved. -// - -import Foundation -import ASN1Swift - -class _InAppReceipt -{ - private var pkcs7container: PKCS7Container - - var payload: InAppReceiptPayload! - - init(rawData: Data) throws - { - let asn1decoder = ASN1Decoder() - self.pkcs7container = try asn1decoder.decode(PKCS7Container.self, from: rawData) - - do - { - self.payload = try asn1decoder.decode(PayloadContainer.self, from: pkcs7container.signedData.contentInfo.payload.rawData).payload - }catch{ - self.payload = try asn1decoder.decode(_PayloadContainer.self, from: pkcs7container.signedData.contentInfo.payload.rawData).payload - } - } -} - -extension _InAppReceipt -{ - var digestAlgorithm: SecKeyAlgorithm? - { - guard let algName = pkcs7container.signedData.alg.items.first?.algorithm else - { - return nil - } - - guard let alg = OID(rawValue: algName)?.encryptionAlgorithm() else - { - return nil - } - - return alg - } - - var worldwideDeveloperCertificateData: Data? - { - let arr = pkcs7container.signedData.certificates.certificates - - guard arr.count >= 2 else - { - return nil - } - - return arr[1].rawData - } - - var signatureData: Data - { - return pkcs7container.signedData.signerInfos.encryptedDigest - } - - var iTunesCertificateContainer: PKCS7Container.Certificate? - { - return pkcs7container.signedData.certificates.certificates.first - } - - var iTunesCertificateData: Data? - { - return iTunesCertificateContainer?.rawData - } - - var iTunesPublicKeyData: Data? - { - return iTunesCertificateContainer?.cert.subjectPublicKeyInfo - } -} - -extension InAppReceiptPayload: ASN1Decodable -{ - static var template: ASN1Template - { - return ASN1Template.universal(ASN1Identifier.Tag.octetString) - } - - enum CodingKeys: ASN1CodingKey - { - case set - - var template: ASN1Template - { - return ASN1Template.universal(ASN1Identifier.Tag.set).constructed() - } - } - - init(from decoder: Decoder) throws - { - let asn1d = decoder as! ASN1DecoderProtocol - - let rawData: Data = try asn1d.extractValueData() - var bundleIdentifier = "" - var bundleIdentifierData = Data() - var appVersion = "" - var originalAppVersion = "" - var originalPurchaseDate: Date? - var purchases = [InAppPurchase]() - var opaqueValue = Data() - var receiptHash = Data() - var expirationDate: Date? - var receiptCreationDate: Date! - var ageRating: String = "" - var environment: String = "" - - let c = try decoder.container(keyedBy: CodingKeys.self) - var container = try c.nestedUnkeyedContainer(forKey: .set) as! ASN1UnkeyedDecodingContainerProtocol - - while !container.isAtEnd - { - do - { - var attributeContainer = try container.nestedUnkeyedContainer(for: ReceiptAttribute.template) as! ASN1UnkeyedDecodingContainerProtocol - let type: Int32 = try attributeContainer.decode(Int32.self) - let _ = try attributeContainer.skip(template: .universal(ASN1Identifier.Tag.integer)) // Consume - var valueContainer = try attributeContainer.nestedUnkeyedContainer(for: .universal(ASN1Identifier.Tag.octetString)) as! ASN1UnkeyedDecodingContainerProtocol - - switch type - { - case InAppReceiptField.bundleIdentifier: - bundleIdentifier = try valueContainer.decode(String.self) - bundleIdentifierData = valueContainer.valueData - case InAppReceiptField.appVersion: - appVersion = try valueContainer.decode(String.self) - case InAppReceiptField.opaqueValue: - opaqueValue = valueContainer.valueData - case InAppReceiptField.receiptHash: - receiptHash = valueContainer.valueData - case InAppReceiptField.inAppPurchaseReceipt: - purchases.append(try valueContainer.decode(InAppPurchase.self)) - case InAppReceiptField.originalAppVersion: - originalAppVersion = try valueContainer.decode(String.self) - case InAppReceiptField.originalAppPurchaseDate: - let originalPurchaseDateString = try valueContainer.decode(String.self, template: .universal(ASN1Identifier.Tag.ia5String)) - originalPurchaseDate = originalPurchaseDateString.rfc3339date() - case InAppReceiptField.expirationDate: - let expirationDateString = try valueContainer.decode(String.self, template: .universal(ASN1Identifier.Tag.ia5String)) - expirationDate = expirationDateString.rfc3339date() - case InAppReceiptField.receiptCreationDate: - let receiptCreationDateString = try valueContainer.decode(String.self, template: .universal(ASN1Identifier.Tag.ia5String)) - receiptCreationDate = receiptCreationDateString.rfc3339date() - case InAppReceiptField.ageRating: - ageRating = try valueContainer.decode(String.self, template: .universal(ASN1Identifier.Tag.ia5String)) - case InAppReceiptField.environment: - environment = try valueContainer.decode(String.self) - default: - break - } - }catch{ - assertionFailure("Something wrong here") - } - } - - self.init(bundleIdentifier: bundleIdentifier, - appVersion: appVersion, - originalAppVersion: originalAppVersion, - originalPurchaseDate: originalPurchaseDate, - purchases: purchases, - expirationDate: expirationDate, - bundleIdentifierData: bundleIdentifierData, - opaqueValue: opaqueValue, - receiptHash: receiptHash, - creationDate: receiptCreationDate, - ageRating: ageRating, - environment: environment, - rawData: rawData) - } -} - -extension InAppPurchase: ASN1Decodable -{ - public init(from decoder: Decoder) throws - { - var container = try decoder.unkeyedContainer() as! ASN1UnkeyedDecodingContainerProtocol - - var originalTransactionIdentifier = "" - var productIdentifier = "" - var transactionIdentifier = "" - var purchaseDate: Date! - var originalPurchaseDate: Date! - - while !container.isAtEnd - { - do - { - var attributeContainer = try container.nestedUnkeyedContainer(for: ReceiptAttribute.template) as! ASN1UnkeyedDecodingContainerProtocol - let type: Int32 = try attributeContainer.decode(Int32.self) - let _ = try attributeContainer.skip(template: .universal(ASN1Identifier.Tag.integer)) // Consume - var valueContainer = try attributeContainer.nestedUnkeyedContainer(for: .universal(ASN1Identifier.Tag.octetString)) as! ASN1UnkeyedDecodingContainerProtocol - - switch type - { - case InAppReceiptField.quantity: - quantity = try valueContainer.decode(Int.self) - case InAppReceiptField.productIdentifier: - productIdentifier = try valueContainer.decode(String.self) - case InAppReceiptField.productType: - productType = Type(rawValue: try valueContainer.decode(Int32.self)) ?? .unknown - case InAppReceiptField.transactionIdentifier: - transactionIdentifier = try valueContainer.decode(String.self) - case InAppReceiptField.purchaseDate: - let purchaseDateString = try valueContainer.decode(String.self, template: .universal(ASN1Identifier.Tag.ia5String)) - purchaseDate = purchaseDateString.rfc3339date() - case InAppReceiptField.originalTransactionIdentifier: - originalTransactionIdentifier = try valueContainer.decode(String.self) - case InAppReceiptField.originalPurchaseDate: - let originalPurchaseDateString = try valueContainer.decode(String.self, template: .universal(ASN1Identifier.Tag.ia5String)) - originalPurchaseDate = originalPurchaseDateString.rfc3339date() - case InAppReceiptField.subscriptionExpirationDate: - let str = try valueContainer.decode(String.self, template: .universal(ASN1Identifier.Tag.ia5String)) - let subscriptionExpirationDateString = str == "" ? nil : str - subscriptionExpirationDate = subscriptionExpirationDateString?.rfc3339date() - case InAppReceiptField.cancellationDate: - let str = try valueContainer.decode(String.self, template: .universal(ASN1Identifier.Tag.ia5String)) - let cancellationDateString = str == "" ? nil : str - cancellationDate = cancellationDateString?.rfc3339date() - case InAppReceiptField.webOrderLineItemID: - webOrderLineItemID = try valueContainer.decode(Int.self) - case InAppReceiptField.subscriptionTrialPeriod: - subscriptionTrialPeriod = (try valueContainer.decode(Int32.self)) != 0 - case InAppReceiptField.subscriptionIntroductoryPricePeriod: - subscriptionIntroductoryPricePeriod = (try valueContainer.decode(Int32.self)) != 0 - case InAppReceiptField.promotionalOfferIdentifier: - promotionalOfferIdentifier = try valueContainer.decode(String.self) - default: - break - } - } - } - - self.originalTransactionIdentifier = originalTransactionIdentifier - self.productIdentifier = productIdentifier - self.transactionIdentifier = transactionIdentifier - self.purchaseDate = purchaseDate - self.originalPurchaseDate = originalPurchaseDate - } - - public static var template: ASN1Template - { - return ASN1Template.universal(ASN1Identifier.Tag.set).constructed() - } -} - -struct ReceiptAttribute: ASN1Decodable -{ - static var template: ASN1Template - { - return ASN1Template.universal(ASN1Identifier.Tag.sequence).constructed() - } - - var type: Int - var version: Int - var value: Data - - enum CodingKeys: ASN1CodingKey - { - case type - case version - case value - - var template: ASN1Template - { - switch self - { - case .type: - return .universal(ASN1Identifier.Tag.integer) - case .version: - return .universal(ASN1Identifier.Tag.integer) - case .value: - return .universal(ASN1Identifier.Tag.octetString) - } - } - } -} - -struct PayloadContainer: ASN1Decodable -{ - var payload: InAppReceiptPayload - - static var template: ASN1Template - { - return ASN1Template.contextSpecific(0).constructed().explicit(tag: ASN1Identifier.Tag.octetString).constructed() - } - - enum CodingKeys: ASN1CodingKey - { - case payload - - var template: ASN1Template - { - switch self - { - case .payload: - return InAppReceiptPayload.template - } - } - } -} - -/// Legacy payload format -struct _PayloadContainer: ASN1Decodable -{ - var payload: InAppReceiptPayload - - static var template: ASN1Template - { - return ASN1Template.contextSpecific(0).constructed() - } - - enum CodingKeys: ASN1CodingKey - { - case payload - - var template: ASN1Template - { - switch self - { - case .payload: - return InAppReceiptPayload.template - } - } - } -} diff --git a/Sources/InAppReceipt.swift b/Sources/InAppReceipt.swift deleted file mode 100644 index df0c633..0000000 --- a/Sources/InAppReceipt.swift +++ /dev/null @@ -1,317 +0,0 @@ -// -// InAppReceipt.swift -// TPReceiptValidator -// -// Created by Pavel Tikhonenko on 28/09/16. -// Copyright © 2016-2021 Pavel Tikhonenko. All rights reserved. -// - -import Foundation -import ASN1Swift - -public struct InAppReceiptField -{ - static let environment: Int32 = 0 // Sandbox, Production, ProductionSandbox - static let bundleIdentifier: Int32 = 2 - static let appVersion: Int32 = 3 - static let opaqueValue: Int32 = 4 - static let receiptHash: Int32 = 5 // SHA-1 Hash - static let ageRating: Int32 = 10 // SHA-1 Hash - static let receiptCreationDate: Int32 = 12 - static let inAppPurchaseReceipt: Int32 = 17 // The receipt for an in-app purchase. - static let originalAppPurchaseDate: Int32 = 18 - static let originalAppVersion: Int32 = 19 - static let expirationDate: Int32 = 21 - - - static let quantity: Int32 = 1701 - static let productIdentifier: Int32 = 1702 - static let transactionIdentifier: Int32 = 1703 - static let purchaseDate: Int32 = 1704 - static let originalTransactionIdentifier: Int32 = 1705 - static let originalPurchaseDate: Int32 = 1706 - static let productType: Int32 = 1707 - static let subscriptionExpirationDate: Int32 = 1708 - static let webOrderLineItemID: Int32 = 1711 - static let cancellationDate: Int32 = 1712 - static let subscriptionTrialPeriod: Int32 = 1713 - static let subscriptionIntroductoryPricePeriod: Int32 = 1719 - static let promotionalOfferIdentifier: Int32 = 1721 -} - -public class InAppReceipt -{ - /// PKCS7 container - internal var receipt: _InAppReceipt - - /// Payload of the receipt. - /// Payload object contains all meta information. - internal var payload: InAppReceiptPayload { receipt.payload } - - /// root certificate path, used to check signature - /// added for testing purpose , as unit test can't read main bundle - internal var rootCertificatePath: String? - - /// Raw data - private var rawData: Data - - /// Initialize a `InAppReceipt` using local receipt - public convenience init() throws - { - let data = try Bundle.main.appStoreReceiptData() - try self.init(receiptData: data) - } - - /// - /// - /// Initialize a `InAppReceipt` with asn1 payload - /// - /// - parameter receiptData: `Data` object that represents receipt - public init(receiptData: Data, rootCertPath: String? = nil) throws - { - self.receipt = try _InAppReceipt(rawData: receiptData) - self.rawData = receiptData - - #if DEBUG - let certificateName = "StoreKitTestCertificate" - #else - let certificateName = "AppleIncRootCertificate" - #endif - - self.rootCertificatePath = rootCertPath ?? Bundle.lookUp(forResource: certificateName, ofType: "cer") - } -} - -public extension InAppReceipt -{ - /// The app’s bundle identifier - var bundleIdentifier: String - { - return payload.bundleIdentifier - } - - /// The app’s version number - var appVersion: String - { - return payload.appVersion - } - - /// The version of the app that was originally purchased. - var originalAppVersion: String - { - return payload.originalAppVersion - } - - /// The date of the app that was originally purchased. - var originalPurchaseDate: Date? - { - return payload.originalPurchaseDate - } - - /// In-app purchase's receipts - var purchases: [InAppPurchase] - { - return payload.purchases - } - - /// Returns all auto renewable `InAppPurchase`s, - var autoRenewablePurchases: [InAppPurchase] - { - return purchases.filter({ $0.isRenewableSubscription }) - } - - /// Returns all ACTIVE auto renewable `InAppPurchase`s, - /// - var activeAutoRenewableSubscriptionPurchases: [InAppPurchase] - { - return purchases.filter({ $0.isRenewableSubscription && $0.isActiveAutoRenewableSubscription(forDate: Date()) }) - - } - - /// The date that the app receipt expires - var expirationDate: Date? - { - return payload.expirationDate - } - - /// Returns `true` if any purchases exist, `false` otherwise - var hasPurchases: Bool - { - return purchases.count > 0 - } - - /// Returns `true` if any Active Auto Renewable purchases exist, `false` otherwise - var hasActiveAutoRenewablePurchases: Bool - { - return activeAutoRenewableSubscriptionPurchases.count > 0 - } - - - var creationDate: Date - { - return payload.creationDate - } - - var ageRating: String - { - return payload.ageRating - } - - /// In App Receipt in base64 - var base64: String - { - return rawData.base64EncodedString() - } - - /// Return original transaction identifier if there is a purchase for a specific product identifier - /// - /// - parameter productIdentifier: Product name - func originalTransactionIdentifier(ofProductIdentifier productIdentifier: String) -> String? - { - return purchases(ofProductIdentifier: productIdentifier).first?.originalTransactionIdentifier - } - - /// Returns `true` if there is a purchase for a specific product identifier, `false` otherwise - /// - /// - parameter productIdentifier: Product name - func containsPurchase(ofProductIdentifier productIdentifier: String) -> Bool - { - for item in purchases - { - if item.productIdentifier == productIdentifier - { - return true - } - } - - return false - } - - /// Returns `[InAppPurchase]` if there are purchases for a specific product identifier, - /// empty array otherwise - /// - /// - parameter productIdentifier: Product name - /// - parameter sort: Sorting block - func purchases(ofProductIdentifier productIdentifier: String, - sortedBy sort: ((InAppPurchase, InAppPurchase) -> Bool)? = nil) -> [InAppPurchase] - { - let filtered: [InAppPurchase] = purchases.filter({ - return $0.productIdentifier == productIdentifier - }) - - if let sort = sort - { - return filtered.sorted(by: { - return sort($0, $1) - }) - }else{ - return filtered.sorted(by: { - return $0.purchaseDate > $1.purchaseDate - }) - } - } - - /// Returns `InAppPurchase` if there is a purchase for a specific product identifier, - /// `nil` otherwise - /// - /// - parameter productIdentifier: Product name - func activeAutoRenewableSubscriptionPurchases(ofProductIdentifier productIdentifier: String, forDate date: Date) -> InAppPurchase? - { - let filtered = purchases(ofProductIdentifier: productIdentifier) - - for purchase in filtered - { - if purchase.isActiveAutoRenewableSubscription(forDate: date) - { - return purchase - } - } - - return nil - } - - /// Returns the last `InAppPurchase` if there is one for a specific product identifier, - /// `nil` otherwise - /// - /// - parameter productIdentifier: Product name - func lastAutoRenewableSubscriptionPurchase(ofProductIdentifier productIdentifier: String) -> InAppPurchase? - { - var purchase: InAppPurchase? = nil - let filtered = purchases(ofProductIdentifier: productIdentifier) - - var lastInterval: TimeInterval = 0 - for iap in filtered - { - if let thisInterval = iap.subscriptionExpirationDate?.timeIntervalSince1970 - { - if purchase == nil || thisInterval > lastInterval - { - purchase = iap - lastInterval = thisInterval - } - } - } - - return purchase - } - - /// Returns true if there is an active subscription for a specific product identifier on the date specified, - /// false otherwise - /// - /// - parameter productIdentifier: Product name - /// - parameter date: Date to check subscription against - func hasActiveAutoRenewableSubscription(ofProductIdentifier productIdentifier: String, forDate date: Date) -> Bool - { - return activeAutoRenewableSubscriptionPurchases(ofProductIdentifier: productIdentifier, forDate: date) != nil - } -} - -internal extension InAppReceipt -{ - /// Used to validate the receipt - var bundleIdentifierData: Data - { - return payload.bundleIdentifierData - } - - /// An opaque value used, with other data, to compute the SHA-1 hash during validation. - var opaqueValue: Data - { - return payload.opaqueValue - } - - /// A SHA-1 hash, used to validate the receipt. - var receiptHash: Data - { - return payload.receiptHash - } -} - -public extension InAppReceipt -{ - /// signature for validation - var signature: Data? - { - return receipt.signatureData - } - - var worldwideDeveloperCertificateData: Data? - { - return receipt.worldwideDeveloperCertificateData - } - - var iTunesCertificateData: Data? - { - return receipt.iTunesCertificateData - } - - var iTunesPublicKeyData: Data? - { - return receipt.iTunesPublicKeyData - } - - var payloadRawData: Data - { - return payload.rawData - } -} diff --git a/Sources/InAppReceiptPayload.swift b/Sources/InAppReceiptPayload.swift deleted file mode 100644 index 8e8a557..0000000 --- a/Sources/InAppReceiptPayload.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// InAppReceiptPayload.swift -// TPInAppReceipt -// -// Created by Pavel Tikhonenko on 20/01/17. -// Copyright © 2017-2021 Pavel Tikhonenko. All rights reserved. -// - -import Foundation -import ASN1Swift - -struct InAppReceiptPayload -{ - /// In-app purchase's receipts - let purchases: [InAppPurchase] - - /// The app’s bundle identifier - let bundleIdentifier: String - - /// The app’s version number - let appVersion: String - - /// The version of the app that was originally purchased. - let originalAppVersion: String - - /// The date when the app orginaly purchased. - let originalPurchaseDate: Date? - - /// The date that the app receipt expires - let expirationDate: Date? - - /// Used to validate the receipt - let bundleIdentifierData: Data - - /// An opaque value used, with other data, to compute the SHA-1 hash during validation. - let opaqueValue: Data - - /// A SHA-1 hash, used to validate the receipt. - let receiptHash: Data - - /// The date when the app receipt was created. - let creationDate: Date - - /// Age Rating of the app - let ageRating: String - - /// Receipt's environment - let environment: String - - /// Raw payload data - let rawData: Data - - /// Initialize a `InAppReceipt` passing all values - /// - init(bundleIdentifier: String, appVersion: String, originalAppVersion: String, originalPurchaseDate: Date?, purchases: [InAppPurchase], expirationDate: Date?, bundleIdentifierData: Data, opaqueValue: Data, receiptHash: Data, creationDate: Date, ageRating: String, environment: String, rawData: Data) - { - self.bundleIdentifier = bundleIdentifier - self.appVersion = appVersion - self.originalAppVersion = originalAppVersion - self.originalPurchaseDate = originalPurchaseDate - self.purchases = purchases - self.expirationDate = expirationDate - self.bundleIdentifierData = bundleIdentifierData - self.opaqueValue = opaqueValue - self.receiptHash = receiptHash - self.creationDate = creationDate - self.ageRating = ageRating - self.environment = environment - self.rawData = rawData - } -} diff --git a/Sources/Initialization.swift b/Sources/Initialization.swift deleted file mode 100644 index 41c0457..0000000 --- a/Sources/Initialization.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// Initialization.swift -// TPInAppReceipt -// -// Created by Pavel Tikhonenko on 05/02/17. -// Copyright © 2017-2021 Pavel Tikhonenko. All rights reserved. -// - -import Foundation - -/// An InAppReceipt extension helps to initialize the receipt -public extension InAppReceipt -{ - /// Creates and returns the 'InAppReceipt' instance from data object - /// - /// - Returns: 'InAppReceipt' instance - /// - throws: An error in the InAppReceipt domain, if `InAppReceipt` cannot be created. - static func receipt(from data: Data) throws -> InAppReceipt - { - return try InAppReceipt(receiptData: data) - } - - /// Creates and returns the 'InAppReceipt' instance using local receipt - /// - /// - Returns: 'InAppReceipt' instance - /// - throws: An error in the InAppReceipt domain, if `InAppReceipt` cannot be created. - static func localReceipt() throws -> InAppReceipt - { - let data = try Bundle.main.appStoreReceiptData() - return try InAppReceipt.receipt(from: data) - } - -} - -/// A Bundle extension helps to retrieve receipt data -public extension Bundle -{ - /// Retrieve local App Store Receipt Data - /// - /// - Returns: 'Data' object that represents local receipt - /// - throws: An error if receipt file not found or 'Data' can't be created - func appStoreReceiptData() throws -> Data - { - guard let receiptUrl = appStoreReceiptURL, - FileManager.default.fileExists(atPath: receiptUrl.path) else - { - throw IARError.initializationFailed(reason: .appStoreReceiptNotFound) - } - - do { - return try Data(contentsOf: receiptUrl) - }catch{ - throw IARError.initializationFailed(reason: .appStoreReceiptNotFound) - } - } - - /// Retrieve local App Store Receipt Data in base64 string - /// - /// - Returns: 'Data' object that represents local receipt - /// - throws: An error if receipt file not found or 'Data' can't be created - func appStoreReceiptBase64() throws -> String - { - return try appStoreReceiptData().base64EncodedString() - } - - class func lookUp(forResource name: String, ofType ext: String?) -> String? - { - - if let p = Bundle.module.path(forResource: name, ofType: ext) - { - return p - } - - if let p = Bundle.main.path(forResource: name, ofType: ext) - { - return p - } - - for f in Bundle.allFrameworks - { - if let identifier = f.bundleIdentifier, identifier.contains("TPInAppReceipt"), - let p = f.path(forResource: name, ofType: ext) - { - return p - } - } - - return nil - } -} - diff --git a/Sources/Objc/InAppReceipt+Objc.swift b/Sources/Objc/InAppReceipt+Objc.swift deleted file mode 100644 index a3c74c8..0000000 --- a/Sources/Objc/InAppReceipt+Objc.swift +++ /dev/null @@ -1,390 +0,0 @@ -// -// InAppReceipt+Objc.swift -// TPInAppReceipt -// -// Created by Pavel Tikhonenko on 24.05.2021. -// Copyright © 2020-2021 Pavel Tikhonenko. All rights reserved. -// - -import Foundation -import TPInAppReceipt - -// MARK: - InAppReceipt - -@objc(InAppReceipt) public class InAppReceipt_Objc: NSObject -{ - private var wrappedReceipt: InAppReceipt - - /// Creates and returns the 'InAppReceipt' instance from data object - /// - /// - Returns: 'InAppReceipt' instance - /// - throws: An error in the InAppReceipt domain, if `InAppReceipt` cannot be created. - @objc public class func receipt(from data: Data) throws -> InAppReceipt_Objc - { - return try InAppReceipt_Objc(receiptData: data) - } - - /// Creates and returns the 'InAppReceipt' instance from data object - /// - /// - Returns: 'InAppReceipt' instance - /// - throws: An error in the InAppReceipt domain, if `InAppReceipt` cannot be created. - @objc public class func receipt(from data: Data) -> InAppReceipt_Objc? - { - return try? InAppReceipt_Objc(receiptData: data) - } - - /// Creates and returns the 'InAppReceipt' instance using local receipt - /// - /// - Returns: 'InAppReceipt' instance - /// - throws: An error in the InAppReceipt domain, if `InAppReceipt` cannot be created. - @objc public class func local() throws -> InAppReceipt_Objc - { - let data = try Bundle.main.appStoreReceiptData() - return try InAppReceipt_Objc.receipt(from: data) - } - - /// Creates and returns the 'InAppReceipt' instance using local receipt - /// - /// - Returns: 'InAppReceipt' instance - /// - throws: An error in the InAppReceipt domain, if `InAppReceipt` cannot be created. - @objc public class func local() -> InAppReceipt_Objc? - { - guard let data = try? Bundle.main.appStoreReceiptData() else { return nil } - return InAppReceipt_Objc.receipt(from: data) - } - - /// - /// - /// Initialize a `InAppReceipt` with asn1 payload - /// - /// - parameter receiptData: `Data` object that represents receipt - @objc public init(receiptData: Data, rootCertPath: String? = nil) throws - { - self.wrappedReceipt = try InAppReceipt.init(receiptData: receiptData, rootCertPath: rootCertPath) - } -} - -@objc public extension InAppReceipt_Objc -{ - /// The app’s bundle identifier - var bundleIdentifier: String - { - return wrappedReceipt.bundleIdentifier - } - - /// The app’s version number - var appVersion: String - { - return wrappedReceipt.appVersion - } - - /// The version of the app that was originally purchased. - var originalAppVersion: String - { - return wrappedReceipt.originalAppVersion - } - - /// The date of the app that was originally purchased. - var originalPurchaseDate: Date? - { - return wrappedReceipt.originalPurchaseDate - } - - /// In-app purchase's receipts - var purchases: [InAppPurchase_Objc] - { - return wrappedReceipt.purchases.map { .init(purchase: $0) } - } - - /// Returns all auto renewable `InAppPurchase`s, - var autoRenewablePurchases: [InAppPurchase_Objc] - { - return wrappedReceipt.purchases.filter({ $0.isRenewableSubscription }).map { .init(purchase: $0) } - } - - /// Returns all ACTIVE auto renewable `InAppPurchase`s, - /// - var activeAutoRenewableSubscriptionPurchases: [InAppPurchase_Objc] - { - return wrappedReceipt.purchases.filter({ $0.isRenewableSubscription && $0.isActiveAutoRenewableSubscription(forDate: Date()) }).map { .init(purchase: $0) } - - } - - /// The date that the app receipt expires - var expirationDate: Date? - { - return wrappedReceipt.expirationDate - } - - /// Returns `true` if any purchases exist, `false` otherwise - var hasPurchases: Bool - { - return purchases.count > 0 - } - - /// Returns `true` if any Active Auto Renewable purchases exist, `false` otherwise - var hasActiveAutoRenewablePurchases: Bool - { - return activeAutoRenewableSubscriptionPurchases.count > 0 - } - - - var creationDate: Date - { - return wrappedReceipt.creationDate - } - - var ageRating: String - { - return wrappedReceipt.ageRating - } - /// In App Receipt in base64 - var base64: String - { - return wrappedReceipt.base64 - } - - /// Return original transaction identifier if there is a purchase for a specific product identifier - /// - /// - parameter productIdentifier: Product name - @objc func originalTransactionIdentifier(ofProductIdentifier productIdentifier: String) -> String? - { - return purchases(ofProductIdentifier: productIdentifier).first?.originalTransactionIdentifier - } - - /// Returns `true` if there is a purchase for a specific product identifier, `false` otherwise - /// - /// - parameter productIdentifier: Product name - @objc func containsPurchase(ofProductIdentifier productIdentifier: String) -> Bool - { - for item in purchases - { - if item.productIdentifier == productIdentifier - { - return true - } - } - - return false - } - - /// Returns `[InAppPurchase]` if there are purchases for a specific product identifier, - /// empty array otherwise - /// - /// - parameter productIdentifier: Product name - /// - parameter sort: Sorting block - @objc func purchases(ofProductIdentifier productIdentifier: String, - sortedBy sort: ((InAppPurchase_Objc, InAppPurchase_Objc) -> Bool)? = nil) -> [InAppPurchase_Objc] - { - let filtered: [InAppPurchase_Objc] = purchases.filter({ - return $0.productIdentifier == productIdentifier - }) - - if let sort = sort - { - return filtered.sorted(by: { - return sort($0, $1) - }) - }else{ - return filtered.sorted(by: { - return $0.purchaseDate > $1.purchaseDate - }) - } - } - - /// Returns `InAppPurchase` if there is a purchase for a specific product identifier, - /// `nil` otherwise - /// - /// - parameter productIdentifier: Product name - @objc func activeAutoRenewableSubscriptionPurchases(ofProductIdentifier productIdentifier: String, forDate date: Date) -> InAppPurchase_Objc? - { - let filtered = purchases(ofProductIdentifier: productIdentifier) - - for purchase in filtered - { - if purchase.isActiveAutoRenewableSubscription(forDate: date) - { - return purchase - } - } - - return nil - } - - /// Returns the last `InAppPurchase` if there is one for a specific product identifier, - /// `nil` otherwise - /// - /// - parameter productIdentifier: Product name - @objc func lastAutoRenewableSubscriptionPurchase(ofProductIdentifier productIdentifier: String) -> InAppPurchase_Objc? - { - var purchase: InAppPurchase_Objc? = nil - let filtered = purchases(ofProductIdentifier: productIdentifier) - - var lastInterval: TimeInterval = 0 - for iap in filtered - { - if !(iap.productIdentifier == productIdentifier) { - continue - } - - if let thisInterval = iap.subscriptionExpirationDate?.timeIntervalSince1970 { - if purchase == nil || thisInterval > lastInterval { - purchase = iap - lastInterval = thisInterval - } - } - } - - return purchase - } - - /// Returns true if there is an active subscription for a specific product identifier on the date specified, - /// false otherwise - /// - /// - parameter productIdentifier: Product name - /// - parameter date: Date to check subscription against - @objc func hasActiveAutoRenewableSubscription(ofProductIdentifier productIdentifier: String, forDate date: Date) -> Bool - { - return activeAutoRenewableSubscriptionPurchases(ofProductIdentifier: productIdentifier, forDate: date) != nil - } -} - -// MARK: - InAppPurchase - -@objc(InAppPurchase) public class InAppPurchase_Objc: NSObject -{ - @objc public enum `Type`: Int32 - { - /// Type that we can't recognize for some reason - case unknown = -1 - - /// Type that customers purchase once. They don't expire. - case nonConsumable - - /// Type that are depleted after one use. Customers can purchase them multiple times. - case consumable - - /// Type that customers purchase once and that renew automatically on a recurring basis until customers decide to cancel. - case nonRenewingSubscription - - /// Type that customers purchase and it provides access over a limited duration and don't renew automatically. Customers can purchase them again. - case autoRenewableSubscription - } - - private let purchase: InAppPurchase - - /// The product identifier which purchase related to - @objc public var productIdentifier: String { purchase.productIdentifier } - - /// Product type - @objc public var productType: Type { Type(rawValue: purchase.productType.rawValue) ?? .unknown } - - /// Transaction identifier - @objc public var transactionIdentifier: String { purchase.transactionIdentifier } - - /// Original Transaction identifier - @objc public var originalTransactionIdentifier: String { purchase.originalTransactionIdentifier } - - /// Purchase Date - @objc public var purchaseDate: Date { purchase.purchaseDate } - - /// Original Purchase Date - @objc public var originalPurchaseDate: Date { purchase.originalPurchaseDate } - - /// Subscription Expiration Date. Returns `nil` if the purchase has been expired (in some cases) - @objc public var subscriptionExpirationDate: Date? { purchase.subscriptionExpirationDate } - - /// Cancellation Date. Returns a value if the transaction was refunded by customer support or the auto-renewable subscription plan was upgraded. Otherwise returns nil. - @objc public var cancellationDate: Date? { purchase.cancellationDate } - - /// This value is `true`if the customer’s subscription is currently in the free trial period, or `false` if not. - @objc public var subscriptionTrialPeriod: Bool { purchase.subscriptionTrialPeriod } - - /// This value is `true` if the customer’s subscription is currently in an introductory price period, or `false` if not. - @objc public var subscriptionIntroductoryPricePeriod: Bool { purchase.subscriptionIntroductoryPricePeriod } - - /// A unique identifier for purchase events across devices, including subscription-renewal events. This value is the primary key for identifying subscription purchases. - @objc public var webOrderLineItemID: Int { purchase.webOrderLineItemID ?? NSNotFound } - - /// The value is an identifier of the subscription offer that the user redeemed. - /// Returns `nil` if the user didn't use any subscription offers. - @objc public var promotionalOfferIdentifier: String? { purchase.promotionalOfferIdentifier } - - /// The number of consumable products purchased - /// The default value is `1` unless modified with a mutable payment. The maximum value is 10. - @objc public var quantity: Int { purchase.quantity } - - init(purchase: InAppPurchase) - { - self.purchase = purchase - } -} - -@objc public extension InAppPurchase_Objc -{ - /// A Boolean value indicating whether the purchase is renewable subscription. - @objc var isRenewableSubscription: Bool - { - return purchase.isRenewableSubscription - } - - /// Check whether the subscription is active for a specific date - /// - /// - Parameter date: The date in which the auto-renewable subscription should be active. - /// - Returns: true if the latest auto-renewable subscription is active for the given date, false otherwise. - @objc func isActiveAutoRenewableSubscription(forDate date: Date) -> Bool - { - return purchase.isActiveAutoRenewableSubscription(forDate: date) - } -} - -// MARK: - Validation - -/// A InAppReceipt extension helps to validate the receipt -@objc public extension InAppReceipt_Objc -{ - /// Verify In App Receipt - /// - /// - throws: An error in the InAppReceipt domain, if verification fails - @objc func verify() throws - { - try wrappedReceipt.verifyHash() - try wrappedReceipt.verifyBundleIdentifierAndVersion() - try wrappedReceipt.verifySignature() - } - - /// Verify only hash - /// Should be equal to `receiptHash` value - /// - /// - throws: An error in the InAppReceipt domain, if verification fails - @objc func verifyHash() throws - { - try wrappedReceipt.verifyHash() - } - - /// Verify that the bundle identifier in the receipt matches a hard-coded constant containing the CFBundleIdentifier value you expect in the Info.plist file. If they do not match, validation fails. - /// Verify that the version identifier string in the receipt matches a hard-coded constant containing the CFBundleShortVersionString value (for macOS) or the CFBundleVersion value (for iOS) that you expect in the Info.plist file. - /// - /// - /// - throws: An error in the InAppReceipt domain, if verification fails - @objc func verifyBundleIdentifierAndVersion() throws - { - try wrappedReceipt.verifyBundleIdentifierAndVersion() - } - - /// Verify signature inside pkcs7 container - /// - /// - throws: An error in the InAppReceipt domain, if verification can't be completed - @objc func verifySignature() throws - { - try wrappedReceipt.verifySignature() - } - - /// Check whether user is eligible for introductory offer for a specific product - /// - /// - Returns `false` if user isn't eligible for introductory offer, otherwise `true` - @objc func productIsEligibleForIntroductoryOffer(_ productIdentifier: String) -> Bool - { - return wrappedReceipt.isEligibleForIntroductoryOffer(for: productIdentifier) - } -} diff --git a/Sources/AppleIncRootCertificate.cer b/Sources/Resources/AppleIncRootCertificate.cer similarity index 100% rename from Sources/AppleIncRootCertificate.cer rename to Sources/Resources/AppleIncRootCertificate.cer diff --git a/Sources/PrivacyInfo.xcprivacy b/Sources/Resources/PrivacyInfo.xcprivacy similarity index 100% rename from Sources/PrivacyInfo.xcprivacy rename to Sources/Resources/PrivacyInfo.xcprivacy diff --git a/Sources/StoreKitTestCertificate.cer b/Sources/Resources/StoreKitTestCertificate.cer similarity index 100% rename from Sources/StoreKitTestCertificate.cer rename to Sources/Resources/StoreKitTestCertificate.cer diff --git a/Sources/Validation.swift b/Sources/Validation.swift index daedfc0..18faa6d 100644 --- a/Sources/Validation.swift +++ b/Sources/Validation.swift @@ -1,361 +1,112 @@ -// -// Validation.swift -// TPInAppReceipt -// -// Created by Pavel Tikhonenko on 19/01/17. -// Copyright © 2017-2021 Pavel Tikhonenko. All rights reserved. -// +import Foundation +import X509 -#if canImport(UIKit) -import UIKit -#endif +// MARK: - AppReceipt Validation -#if canImport(WatchKit) -import WatchKit -#endif +extension AppReceipt { + /// Validates the receipt asynchronously. + /// + /// Performs validation of this receipt instance including certificate chain verification, + /// signature verification, hash verification, and metadata validation. + /// + /// - Parameters: + /// - rootCertificate: Optional custom root certificate data. If not provided, the appropriate + /// Apple root certificate is selected based on the receipt's environment. + /// - deviceIdentifier: Optional device identifier data. If not provided, the system device + /// identifier is retrieved asynchronously. + /// - Returns: A ``VerificationResult`` indicating whether the receipt is valid or invalid. + @concurrent public func validate( + rootCertificate: Data? = nil, + deviceIdentifier: Data? = nil + ) async -> VerificationResult { + guard hasValidStructure else { + return .invalid(ReceiptValidatorError.invalidReceiptStructure) + } -#if canImport(Cocoa) -import Cocoa -import IOKit -#endif + guard let root = await finalAppleRootCertificateData(using: rootCertificate), + let rootCert = try? Certificate(derEncoded: root) + else { + return .invalid(ReceiptValidatorError.rootCertificateInvalid(nil)) + } -import CommonCrypto + guard let deviceId = await finalDeviceIdentifier(using: deviceIdentifier) else { + return .invalid(ReceiptValidatorError.deviceIdentifierIsNotFound) + } -/// A InAppReceipt extension helps to validate the receipt -public extension InAppReceipt -{ - /// Determine whether receipt is valid or not - /// - /// - Returns:`true` if the receipt is valid, otherwise `false` - var isValid: Bool - { - do { - try validate() - return true - } catch { - return false - } - } - - /// Validate In App Receipt - /// - /// - throws: An error in the InAppReceipt domain, if verification fails - func validate() throws - { - try verifyHash() - try verifyBundleIdentifierAndVersion() - try verifySignature() - } - - /// Verify In App Receipt - /// - /// - throws: An error in the InAppReceipt domain, if verification fails - @available(*, deprecated, renamed: "validate") - func verify() throws - { - try verifyHash() - try verifyBundleIdentifierAndVersion() - try verifySignature() + let validator = ReceiptValidator.default( + rootCertificate: rootCert, + deviceIdentifier: deviceId + ) + + return await validator.validate(self) } - - /// Verify only hash - /// Should be equal to `receiptHash` value - /// - /// - throws: An error in the InAppReceipt domain, if verification fails - func verifyHash() throws - { - if (computedHash != receiptHash) - { - throw IARError.validationFailed(reason: .hashValidation) - } +} + +extension AppReceipt: ReceiptValidatable { + public var bundleIdentifierData: Data { + payload.bundleIdentifierData } - - /// Verify that the bundle identifier in the receipt matches a hard-coded constant containing the CFBundleIdentifier value you expect in the Info.plist file. If they do not match, validation fails. - /// Verify that the version identifier string in the receipt matches a hard-coded constant containing the CFBundleShortVersionString value (for macOS) or the CFBundleVersion value (for iOS) that you expect in the Info.plist file. - /// - /// - /// - throws: An error in the InAppReceipt domain, if verification fails - func verifyBundleIdentifierAndVersion() throws - { - try verifyBundleIdentifier() - try verifyBundleVersion() + + public var opaqueValue: Data { + payload.opaqueValue } - - /// Verify that the bundle identifier in the receipt matches a hard-coded constant containing the CFBundleIdentifier value you expect in the Info.plist file. If they do not match, validation fails. - /// - /// - /// - throws: An error in the InAppReceipt domain, if verification fails - func verifyBundleIdentifier() throws - { - guard let bid = Bundle.main.bundleIdentifier, - bid == bundleIdentifier else - { - throw IARError.validationFailed(reason: .bundleIdentifierVerification) - } - } - - /// Verify that the version identifier string in the receipt matches a hard-coded constant containing the CFBundleShortVersionString value (for macOS) or the CFBundleVersion value (for iOS) that you expect in the Info.plist file. - /// - /// - /// - throws: An error in the InAppReceipt domain, if verification fails - func verifyBundleVersion() throws - { - guard appVersion == Bundle.main.appVersion else - { - throw IARError.validationFailed(reason: .bundleVersionVerification) - } - } - - /// Verify signature inside pkcs7 container - /// - /// - throws: An error in the InAppReceipt domain, if verification can't be completed - func verifySignature() throws - { - try checkAppleRootCertExistence() - try checkSignatureValidity() - try checkChainOfTrust() + + public var receiptHash: Data { + payload.receiptHash } - - /// Verifies existence of Apple Root Certificate in bundle - /// - /// - throws: An error in the InAppReceipt domain, if Apple Root Certificate does not exist - fileprivate func checkAppleRootCertExistence() throws - { - guard let rootCertificatePath, - FileManager.default.fileExists(atPath: rootCertificatePath) else - { - throw IARError.validationFailed(reason: .signatureValidation(.appleIncRootCertificateNotFound)) - } + + public var versionIdentifier: String { + appVersion } - - func checkChainOfTrust() throws - { - // Validate chain of trust of certificate - // Ensure the iTunes certificate included in the receipt is indeed signed by Apple root cert - // https://developer.apple.com/documentation/security/certificate_key_and_trust_services/trust/creating_a_trust_object - - // root cert data is loaded from the bundled Apple Root Certificate - guard let path = rootCertificatePath, - let rootCertData = try? Data(contentsOf: URL(fileURLWithPath: path)) else - { - throw IARError.validationFailed(reason: .signatureValidation(.unableToLoadAppleIncRootCertificate)) - } - - guard let iTunesCertData = iTunesCertificateData else - { - throw IARError.validationFailed(reason: .signatureValidation(.unableToLoadiTunesCertificate)) - } - - guard let worldwideDeveloperCertData = worldwideDeveloperCertificateData else - { - throw IARError.validationFailed(reason: .signatureValidation(.unableToLoadWorldwideDeveloperCertificate)) - } - - guard let rootCertSec = SecCertificateCreateWithData(nil, rootCertData as CFData) else { - throw IARError.validationFailed(reason: .signatureValidation(.unableToLoadAppleIncRootCertificate)) - } - - guard let iTunesCertSec = SecCertificateCreateWithData(nil, iTunesCertData as CFData) else { - throw IARError.validationFailed(reason: .signatureValidation(.unableToLoadiTunesCertificate)) - } - - guard let worldwideDevCertSec = SecCertificateCreateWithData(nil, worldwideDeveloperCertData as CFData) else { - throw IARError.validationFailed(reason: .signatureValidation(.unableToLoadWorldwideDeveloperCertificate)) - } - - let policy = SecPolicyCreateBasicX509() - - var wwdcTrust: SecTrust? - var iTunesTrust: SecTrust? - - // verify worldwide developer cert in the receipt is signed by Apple Root Cert - let worldwideDevCertVerifyStatus = SecTrustCreateWithCertificates([worldwideDevCertSec, rootCertSec] as AnyObject, - policy, - &wwdcTrust) - - guard worldwideDevCertVerifyStatus == errSecSuccess, let wwdcTrust else - { - throw IARError.validationFailed(reason: .signatureValidation(.invalidCertificateChainOfTrust)) - } - - // verify iTunes cert in the receipt is signed by worldwide developer cert, which is signed by Apple Root Cert - let iTunesCertVerifyStatus = SecTrustCreateWithCertificates([iTunesCertSec, worldwideDevCertSec, rootCertSec] as AnyObject, - policy, - &iTunesTrust) - - guard iTunesCertVerifyStatus == errSecSuccess, let iTunesTrust else - { - throw IARError.validationFailed(reason: .signatureValidation(.invalidCertificateChainOfTrust)) - } - - var secTrustResult: SecTrustResultType = SecTrustResultType.unspecified - - if #available(OSX 10.14, iOS 12.0, tvOS 12.0, *) - { - var error: CFError? - guard SecTrustEvaluateWithError(wwdcTrust, &error) else - { - throw IARError.validationFailed(reason: .signatureValidation(.invalidCertificateChainOfTrust)) - } - } else { - guard SecTrustEvaluate(wwdcTrust, &secTrustResult) == errSecSuccess else - { - throw IARError.validationFailed(reason: .signatureValidation(.invalidCertificateChainOfTrust)) - } - } - - if #available(OSX 10.14, iOS 12.0, tvOS 12.0, *) - { - var error: CFError? - guard SecTrustEvaluateWithError(iTunesTrust, &error) else - { - throw IARError.validationFailed(reason: .signatureValidation(.invalidCertificateChainOfTrust)) - } - } else { - guard SecTrustEvaluate(iTunesTrust, &secTrustResult) == errSecSuccess else - { - throw IARError.validationFailed(reason: .signatureValidation(.invalidCertificateChainOfTrust)) - } - } + + public var certificates: [Certificate] { + content.certificates } - - func checkSignatureValidity() throws - { - guard let signature = signature else - { - throw IARError.validationFailed(reason: .signatureValidation(.signatureNotFound)) - } - - guard let iTunesPublicKeyContainer = receipt.iTunesPublicKeyData else - { - throw IARError.validationFailed(reason: .signatureValidation(.unableToLoadiTunesPublicKey)) - } - - let keyDict: [String:Any] = - [ - kSecAttrKeyClass as String: kSecAttrKeyClassPublic, - kSecAttrKeyType as String: kSecAttrKeyTypeRSA, - kSecAttrKeySizeInBits as String: 2048, - ] - guard let iTunesPublicKeySec = SecKeyCreateWithData(iTunesPublicKeyContainer as CFData, keyDict as CFDictionary, nil) else { - throw IARError.validationFailed(reason: .signatureValidation(.unableToLoadAppleIncPublicSecKey)) - } - - var umErrorCF: Unmanaged? = nil - guard let alg = receipt.digestAlgorithm, - SecKeyVerifySignature(iTunesPublicKeySec, alg, payloadRawData as CFData, signature as CFData, &umErrorCF) else { - - let error = umErrorCF?.takeRetainedValue() as Error? as NSError? - print("error is \(String(describing: error))") - - throw IARError.validationFailed(reason: .signatureValidation(.invalidSignature)) - } - + public var certificatesRaw: [Data] { + content.certificatesRaw } - - /// Computed SHA-1 hash, used to validate the receipt. - internal var computedHash: Data - { - let uuidData = guid() - let opaqueData = opaqueValue - let bundleIdData = bundleIdentifierData - - var hash = [UInt8](repeating: 0, count:Int(CC_SHA1_DIGEST_LENGTH)) - var ctx = CC_SHA1_CTX() - CC_SHA1_Init(&ctx) - CC_SHA1_Update(&ctx, uuidData.bytes, CC_LONG(uuidData.count)) - CC_SHA1_Update(&ctx, opaqueData.bytes, CC_LONG(opaqueData.count)) - CC_SHA1_Update(&ctx, bundleIdData.bytes, CC_LONG(bundleIdData.count)) - CC_SHA1_Final(&hash, &ctx) - - return Data(hash) + + public var validationTime: Date { + creationDate } -} -fileprivate func guid() -> Data -{ -#if targetEnvironment(macCatalyst) || os(macOS) - if let guid = getMacAddress() - { - return guid - }else{ - assertionFailure("Failed to retrieve guid") + public var digestData: Data { + payloadRawData } - - return Data() // Never get called -#else -#if canImport(WatchKit) - var uuidBytes = WKInterfaceDevice.current().identifierForVendor!.uuid -#elseif canImport(UIKit) - var uuidBytes = UIDevice.current.identifierForVendor!.uuid -#endif - - return Data(bytes: &uuidBytes, count: MemoryLayout.size(ofValue: uuidBytes)) -#endif -} + public var digestAlgorithm: DigestAlgorithm { + signerInfo.digestAlgorithm == .sha1 ? .sha1 : .sha256 + } + + public var signature: Data { + Data(signerInfo.signature.bytes) + } + + // MARK: - Private Implementation + + var signerInfo: SignerInfo { + guard let signer = content.signerInfos.first else { + fatalError("Invalid Receipt: Expected one signer") + } -#if targetEnvironment(macCatalyst) || os(macOS) -func getMacAddress() -> Data? -{ - guard let service = ioService(named: "en0", wantBuiltIn: true) - ?? ioService(named: "en1", wantBuiltIn: true) - ?? ioService(named: "en0", wantBuiltIn: false) - else { return nil } - - defer { IOObjectRelease(service) } - - if let cftype = IORegistryEntrySearchCFProperty(service, kIOServicePlane, "IOMACAddress" as CFString, kCFAllocatorDefault, IOOptionBits(kIORegistryIterateRecursively | kIORegistryIterateParents)) - { - return (cftype as? Data) - } - - return nil + return signer + } } -func ioService(named name: String, wantBuiltIn: Bool) -> io_service_t? -{ - let main_port: mach_port_t - if #available(macOS 12.0, macCatalyst 15.0, *) { - main_port = kIOMainPortDefault - } else { - main_port = 0 // the kIOMasterPortDefault symbol is unavailable on xcode 14 and later. - } - var iterator = io_iterator_t() - - defer - { - if iterator != IO_OBJECT_NULL - { - IOObjectRelease(iterator) - } - } - - guard let matchingDict = IOBSDNameMatching(main_port, 0, name), - IOServiceGetMatchingServices(main_port, matchingDict as CFDictionary, &iterator) == KERN_SUCCESS, - iterator != IO_OBJECT_NULL - else - { - return nil - } - - var candidate = IOIteratorNext(iterator) - while candidate != IO_OBJECT_NULL - { - if let cftype = IORegistryEntryCreateCFProperty(candidate, "IOBuiltin" as CFString, kCFAllocatorDefault, 0) - { - let isBuiltIn = cftype.takeRetainedValue() as! CFBoolean - if wantBuiltIn == CFBooleanGetValue(isBuiltIn) - { - return candidate - } - } - - IOObjectRelease(candidate) - candidate = IOIteratorNext(iterator) - } - - return nil +extension AppReceipt { + fileprivate func finalAppleRootCertificateData(using rootCertificate: Data? = nil) async -> Data? { + guard let rootCertificate else { + let isXcode = payload.environment == .xcode + return await Bundle.appleRootCertificateData(testing: isXcode) + } + return rootCertificate + } + + fileprivate func finalDeviceIdentifier(using deviceIdentifier: Data?) async -> Data? { + guard let deviceIdentifier else { + return await DeviceIdentifier.data + } + return deviceIdentifier + } } -#endif diff --git a/Sources/Validator/Default Verifiers/Chain/ReceiptChainVerifier.swift b/Sources/Validator/Default Verifiers/Chain/ReceiptChainVerifier.swift new file mode 100644 index 0000000..f273903 --- /dev/null +++ b/Sources/Validator/Default Verifiers/Chain/ReceiptChainVerifier.swift @@ -0,0 +1,71 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +import X509 + +/// Errors that occur during certificate chain verification. +/// +/// Represents failures in validating the X.509 certificate chain from the leaf certificate to the root. +public enum ChainVerificationError: Hashable, Sendable, Error { + /// The certificate data is invalid, malformed, or cannot be decoded. + case invalidCertificateData + + /// The certificate chain validation failed. + /// + /// This can occur when certificates are expired, self-signed, untrusted, or the chain is incomplete. + case chainValidationFailed + + /// The certificate revocation check failed or the certificate has been revoked. + case revocationCheckFailed +} + +/// Policies that control how certificate chain verification is performed. +/// +/// These policies specify validation rules and constraints applied during certificate chain verification. +public enum ReceiptChainVerifierPolicy: Sendable { + /// Use Apple's basic X.509 validation policies for certificate verification. + case appleX509Basic + + /// Use App Store specific validation policies. + /// + /// - Note: This policy doesn't work in Xcode development environments. + case appStoreReceipt + + /// Validate the certificate chain as if it was on the specified date. + /// + /// Ensures certificates were valid on the given date, useful for historical receipt validation. + case validationTime(Date) + + /// Use the specified date for online revocation checks, if any. + case onlineValidationTime(Date) +} + +/// A protocol for types that verify certificate chains in receipts. +/// +/// Conforming types validate the X.509 certificate chain used to sign a receipt, ensuring that +/// the leaf certificate chains up to the Apple root certificate and that all certificates in the +/// chain are valid, properly chained, and not revoked. +/// +/// - Note: All types conforming to this protocol must be `Sendable`. +public protocol ReceiptChainVerifier: Sendable { + /// Verifies the certificate chain asynchronously. + /// + /// Validates that the leaf certificate is properly chained to the intermediate certificate(s), + /// and that the chain is valid according to the specified policies. + /// + /// - Parameters: + /// - leaf: The leaf certificate from the receipt to verify. + /// - intermediate: Array of intermediate certificates in the chain. Should include all + /// certificates between the leaf and the root (typically one intermediate for App Store receipts). + /// - policy: Array of verification policies to apply. Controls how validation is performed + /// (e.g., which X.509 policies to use, what date to validate against). + /// - Returns: A verification result indicating whether the chain is valid or the error that occurred. + func verify( + leaf: Certificate, + intermediate: [Certificate], + policy: [ReceiptChainVerifierPolicy] + ) async -> VerificationResult +} diff --git a/Sources/Validator/Default Verifiers/Chain/Sec_ChainVerifier.swift b/Sources/Validator/Default Verifiers/Chain/Sec_ChainVerifier.swift new file mode 100644 index 0000000..b4676e1 --- /dev/null +++ b/Sources/Validator/Default Verifiers/Chain/Sec_ChainVerifier.swift @@ -0,0 +1,153 @@ +#if canImport(Security) + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@preconcurrency import Security +import X509 + +// MARK: - "Legacy" Chain Verifier + +public struct SecChainVerifier: ReceiptChainVerifier, Sendable { + private let rootCertificates: [Data] + + public init(rootCertificates: [Data]) { + self.rootCertificates = rootCertificates + } + + func verify( + leaf: SecCertificate, + intermediate: [SecCertificate], + policy: [ReceiptChainVerifierPolicy] + ) -> VerificationResult { + var trust: SecTrust? + + var rootSecCertificates: [SecCertificate] = [] + for cert in rootCertificates { + guard let cert = SecCertificateCreateWithData(nil, cert as CFData) else { + return .invalid(ChainVerificationError.invalidCertificateData) + } + rootSecCertificates.append(cert) + } + + var certificates: [SecCertificate] = [leaf] + certificates.append(contentsOf: intermediate) + + let status = SecTrustCreateWithCertificates(certificates as CFArray, nil, &trust) + guard status == errSecSuccess, let trustRef = trust else { + return .invalid(ChainVerificationError.chainValidationFailed) + } + + // Set anchor certificates and use only these anchors (not system roots) + guard SecTrustSetAnchorCertificates(trustRef, rootSecCertificates as CFArray) == errSecSuccess else { + return .invalid(ChainVerificationError.chainValidationFailed) + } + + guard SecTrustSetAnchorCertificatesOnly(trustRef, true) == errSecSuccess else { + return .invalid(ChainVerificationError.chainValidationFailed) + } + + var secPolicies: [SecPolicy] = [] + + for p in policy { + if let secPolicy = p.policy { + secPolicies.append(secPolicy) + } else if case let .validationTime(data) = p { + if SecTrustSetVerifyDate(trustRef, data as CFDate) != errSecSuccess { + return .invalid(ChainVerificationError.chainValidationFailed) + } + } + } + + guard !secPolicies.isEmpty else { + return .invalid(ChainVerificationError.chainValidationFailed) + } + + guard SecTrustSetPolicies(trustRef, secPolicies as CFArray) == errSecSuccess else { + return .invalid(ChainVerificationError.chainValidationFailed) + } + + var error: CFError? + let trusted = SecTrustEvaluateWithError(trustRef, &error) + + guard trusted && error == nil else { + return .invalid(ChainVerificationError.chainValidationFailed) + } + + return .valid + } + + public func verify( + leaf: Certificate, + intermediate: [Certificate], + policy: [ReceiptChainVerifierPolicy] + ) -> VerificationResult { + do { + let leaf = try SecCertificate.makeWithCertificate(leaf) + let intermediate = try intermediate.map { try SecCertificate.makeWithCertificate($0) } + + return verify( + leaf: leaf, + intermediate: intermediate, + policy: policy + ) + } catch { + return .invalid(ChainVerificationError.invalidCertificateData) + } + } +} + +extension SecChainVerifier: ReceiptVerifier { + public func verify(_ receipt: any ReceiptValidatable) -> VerificationResult { + guard let leaf = receipt.leafCertificate else { + return .invalid(ChainVerificationError.invalidCertificateData) + } + + let policy: [ReceiptChainVerifierPolicy] + if receipt.environment == .xcode { + policy = [ + .appleX509Basic, + .validationTime(receipt.validationTime), + ] + } else { + policy = [ + .appleX509Basic, + .appStoreReceipt, + .validationTime(receipt.validationTime), + ] + } + + return verify( + leaf: leaf, + intermediate: receipt.intermediateCertificates, + policy: policy + ) + } +} + +extension ReceiptChainVerifierPolicy { + fileprivate var policy: SecPolicy? { + switch self { + case .appleX509Basic: + return SecPolicyCreateWithProperties(kSecPolicyAppleX509Basic, nil) + case .appStoreReceipt: + return SecPolicyCreateWithProperties(kSecPolicyMacAppStoreReceipt, nil) + case .validationTime: + return nil + case .onlineValidationTime: + let revocationOptions: [CFString: Any] = [ + kSecPolicyRevocationFlags: kSecRevocationOCSPMethod | kSecRevocationCRLMethod + ] + + return SecPolicyCreateWithProperties( + kSecPolicyAppleRevocation, + revocationOptions as CFDictionary + ) + } + } +} + +#endif diff --git a/Sources/Validator/Default Verifiers/Chain/X509_ChainVerifier.swift b/Sources/Validator/Default Verifiers/Chain/X509_ChainVerifier.swift new file mode 100644 index 0000000..cd23ec1 --- /dev/null +++ b/Sources/Validator/Default Verifiers/Chain/X509_ChainVerifier.swift @@ -0,0 +1,318 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +import Crypto +import SwiftASN1 +@_spi(FixedExpiryValidationTime) import X509 + +// MARK: - Chain Verifier + +public final class X509ChainVerifier: ReceiptChainVerifier { + private let store: CertificateStore + + public init(rootCertificates: [Data]) throws { + let parsedCertificates = try rootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) } + store = CertificateStore(parsedCertificates) + } + + public init(rootCertificates: [Certificate]) { + store = CertificateStore(rootCertificates) + } + + public func verify( + leaf: Certificate, + intermediate: [Certificate], + policy: [ReceiptChainVerifierPolicy] + ) async -> VerificationResult { + let result = await verify( + leaf: leaf, + intermediate: intermediate, + rootCertificate: store, + policy: policy + ) + + if case let .couldNotValidate(terminalErrors) = result { + for failure in terminalErrors { + let failureReasonString = failure.policyFailureReason.description + if failureReasonString.contains(Requester.OCSP_NETWORK_REQUEST_FAILED) { + return .invalid(ChainVerificationError.revocationCheckFailed) + } + } + return .invalid(ChainVerificationError.chainValidationFailed) + } + + return .valid + } + + private func verify( + leaf: Certificate, + intermediate: [Certificate], + rootCertificate: CertificateStore, + policy: [ReceiptChainVerifierPolicy], + ) async -> X509.CertificateValidationResult { + var verifier = Verifier(rootCertificates: rootCertificate) { + PolicySequence(policy.compactMap { $0.policy }) + } + + let intermediateStore = CertificateStore(intermediate) + return await verifier.validate(leaf: leaf, intermediates: intermediateStore) + } +} + +extension X509ChainVerifier: ReceiptVerifier { + public func verify(_ receipt: any ReceiptValidatable) async -> VerificationResult { + guard let leaf = receipt.leafCertificate else { + return .invalid(ChainVerificationError.invalidCertificateData) + } + + guard receipt.certificates.count > 1 else { + return .invalid(ChainVerificationError.invalidCertificateData) + } + + let intermediate = receipt.certificates[1] + + let policy: [ReceiptChainVerifierPolicy] + if receipt.environment == .xcode { + policy = [] + } else { + policy = [ + .appleX509Basic, + .appStoreReceipt, + .validationTime(receipt.validationTime), + ] + } + let result = await verify( + leaf: leaf, + intermediate: [intermediate], + policy: policy + ) + + return result + } +} + +final class AppStoreOIDPolicy: VerifierPolicy { + private static let NUMBER_OF_CERTS = 3 + private static let WWDR_INTERMEDIATE_OID: ASN1ObjectIdentifier = [1, 2, 840, 113_635, 100, 6, 2, 1] + private static let RECEIPT_SIGNER_OID: ASN1ObjectIdentifier = [1, 2, 840, 113_635, 100, 6, 11, 1] + + init() { + verifyingCriticalExtensions = [] + } + + var verifyingCriticalExtensions: [SwiftASN1.ASN1ObjectIdentifier] + + func chainMeetsPolicyRequirements(chain: X509.UnverifiedCertificateChain) async -> X509.PolicyEvaluationResult { + if chain.count != AppStoreOIDPolicy.NUMBER_OF_CERTS { + return X509.PolicyEvaluationResult.failsToMeetPolicy(reason: "Chain has unexpected length") + } + let intermediateCertificate = chain[1] + let leafCertificate = chain[0] + if !intermediateCertificate.extensions.contains(where: { ext in + ext.oid == AppStoreOIDPolicy.WWDR_INTERMEDIATE_OID + }) { + return X509.PolicyEvaluationResult.failsToMeetPolicy( + reason: "Intermediate certificate does not contain WWDR OID" + ) + } + if !leafCertificate.extensions.contains(where: { ext in + ext.oid == AppStoreOIDPolicy.RECEIPT_SIGNER_OID + }) { + return X509.PolicyEvaluationResult.failsToMeetPolicy( + reason: "Leaf certificate does not contain Receipt Signing OID" + ) + } + return X509.PolicyEvaluationResult.meetsPolicy + } +} + +// MARK: - OCSP Requester + +#if canImport(Darwin) + +final class Requester: OCSPRequester { + static let OCSP_NETWORK_REQUEST_FAILED = "OCSP_NETWORK_REQUEST_FAILED" + + private let urlSession: URLSession + + convenience init(sessionConfiguration: URLSessionConfiguration = .default) { + self.init(urlSession: URLSession(configuration: sessionConfiguration)) + } + + init(urlSession: URLSession) { + self.urlSession = urlSession + } + + init() { + self.urlSession = URLSession(configuration: .default) + } + + func query(request: [UInt8], uri: String) async -> X509.OCSPRequesterQueryResult { + guard let url = URL(string: uri) else { + return .terminalError(OCSPValidationError.fetchFailed) + } + + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = "POST" + urlRequest.httpBody = Data(request) + urlRequest.setValue("application/ocsp-request", forHTTPHeaderField: "Content-Type") + urlRequest.setValue("application/ocsp-response", forHTTPHeaderField: "Accept") + urlRequest.setValue("\(request.count)", forHTTPHeaderField: "Content-Length") + + do { + let (data, response) = try await urlSession.data(for: urlRequest) + + guard let httpResponse = response as? HTTPURLResponse else { + throw OCSPValidationError.networkError(underlyingError: URLError(.badServerResponse)) + } + + guard (200...299).contains(httpResponse.statusCode) else { + throw OCSPValidationError.httpError(statusCode: UInt(httpResponse.statusCode)) + } + + return .response([UInt8](data)) + } catch let error as OCSPValidationError { + return .terminalError(error) + } catch { + return .terminalError(OCSPValidationError.networkError(underlyingError: error)) + } + } + + enum OCSPValidationError: Error, CustomStringConvertible { + case fetchFailed + case httpError(statusCode: UInt) + case networkError(underlyingError: Error) + + var description: String { + switch self { + case .fetchFailed: + return "\(Requester.OCSP_NETWORK_REQUEST_FAILED): Could not read response body" + case let .httpError(statusCode): + return "\(Requester.OCSP_NETWORK_REQUEST_FAILED): HTTP error \(statusCode)" + case let .networkError(underlyingError): + return + "\(Requester.OCSP_NETWORK_REQUEST_FAILED): Network error - \(underlyingError.localizedDescription)" + } + } + } +} + +#else + +import AsyncHTTPClient + +final class Requester: OCSPRequester { + static let OCSP_NETWORK_REQUEST_FAILED = "OCSP_NETWORK_REQUEST_FAILED" + + private let client: HTTPClient + + init() { + self.client = HTTPClient(eventLoopGroupProvider: .singleton) + } + + func query(request: [UInt8], uri: String) async -> X509.OCSPRequesterQueryResult { + do { + var urlRequest = HTTPClientRequest(url: uri) + urlRequest.method = .POST + urlRequest.headers.add(name: "Content-Type", value: "application/ocsp-request") + urlRequest.body = .bytes(request) + + let response = try await client.execute(urlRequest, timeout: .seconds(30)) + + guard response.status.code == 200 else { + throw OCSPValidationError.httpError(statusCode: UInt(response.status.code)) + } + + var body = try await response.body.collect(upTo: 1024 * 1024) + guard let data = body.readBytes(length: body.readableBytes) else { + throw OCSPValidationError.fetchFailed + } + + return .response(data) + } catch let error as OCSPValidationError { + return .terminalError(error) + } catch { + return .terminalError(OCSPValidationError.networkError(underlyingError: error)) + } + } + + enum OCSPValidationError: Error, CustomStringConvertible { + case fetchFailed + case httpError(statusCode: UInt) + case networkError(underlyingError: Error) + + var description: String { + switch self { + case .fetchFailed: + return "\(Requester.OCSP_NETWORK_REQUEST_FAILED): Could not read response body" + case let .httpError(statusCode): + return "\(Requester.OCSP_NETWORK_REQUEST_FAILED): HTTP error \(statusCode)" + case let .networkError(underlyingError): + return + "\(Requester.OCSP_NETWORK_REQUEST_FAILED): Network error - \(underlyingError.localizedDescription)" + } + } + } +} + +#endif + +extension ReceiptChainVerifierPolicy { + fileprivate var policy: VerifierPolicy? { + switch self { + case .appleX509Basic: + return nil + case .appStoreReceipt: + return AppStoreOIDPolicy() + case let .validationTime(time): + return RFC5280Policy(fixedExpiryValidationTime: time) + case let .onlineValidationTime(time): + return OCSPVerifierPolicy( + failureMode: .soft, + requester: Requester(), + fixedExpiryValidationTime: time + ) + } + } +} + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *) +public struct PolicySequence: VerifierPolicy { + public var policies: [AnyPolicy] + + public init(_ policies: [AnyPolicy]) { + self.policies = policies + } + + public init(_ policies: [any VerifierPolicy]) { + self.policies = policies.map { AnyPolicy($0) } + } + + public var verifyingCriticalExtensions: [ASN1ObjectIdentifier] { + policies.flatMap { $0.verifyingCriticalExtensions } + } + + public mutating func chainMeetsPolicyRequirements( + chain: UnverifiedCertificateChain + ) async -> PolicyEvaluationResult { + for i in policies.indices { + switch await policies[i].chainMeetsPolicyRequirements(chain: chain) { + case .meetsPolicy: + continue + case let .failsToMeetPolicy(reason): + return .failsToMeetPolicy(reason: reason) + } + } + return .meetsPolicy + } +} + +extension PolicyBuilder { + @inlinable + public static func buildArray(_ components: [VerifierPolicy]) -> some VerifierPolicy { + PolicySequence(components.map { AnyPolicy($0) }) + } +} diff --git a/Sources/Validator/Default Verifiers/Hash/DeviceIdentifier.swift b/Sources/Validator/Default Verifiers/Hash/DeviceIdentifier.swift new file mode 100644 index 0000000..ba7cd96 --- /dev/null +++ b/Sources/Validator/Default Verifiers/Hash/DeviceIdentifier.swift @@ -0,0 +1,131 @@ +import Foundation + +// MARK: - Device Identifier + +/// Provides platform-specific device identifiers for receipt hash verification. +public enum DeviceIdentifier { + // Platform-specific implementations below +} + +// MARK: - iOS/tvOS/watchOS Implementation + +#if os(iOS) || os(watchOS) || os(tvOS) +#if canImport(WatchKit) +import WatchKit +#elseif canImport(UIKit) +import UIKit +#endif + +extension DeviceIdentifier { + /// Retrieves the vendor identifier for the current device. + /// + /// - Returns: The device identifier as `Data`, or nil if unavailable. + public static var data: Data? { + get async { + #if canImport(WatchKit) + let uuid = WKInterfaceDevice.current().identifierForVendor + #elseif canImport(UIKit) + let uuid = await UIDevice.current.identifierForVendor + #endif + return uuid?.data + } + } + + @_spi(Blocking) + public static var data_blocking: Data? { + #if canImport(WatchKit) + let uuid = WKInterfaceDevice.current().identifierForVendor + #elseif canImport(UIKit) + let uuid = UIDevice.current.identifierForVendor + #endif + return uuid?.data + } +} +#endif + +// MARK: - macOS Implementation + +#if targetEnvironment(macCatalyst) || os(macOS) +import IOKit + +extension DeviceIdentifier { + /// Retrieves the MAC address for the current device. + /// + /// - Returns: The device identifier as `Data`, or nil if unavailable. + public static var data: Data? { + obtainMACAddress() + } + + @_spi(Blocking) + public static var data_blocking: Data? { + obtainMACAddress() + } + + private static func ioService(named name: String, wantBuiltIn: Bool) -> io_service_t? { + let defaultPort: mach_port_t + + if #available(macOS 12.0, macCatalyst 15.0, *) { + defaultPort = kIOMainPortDefault + } else { + defaultPort = 0 + } + + var iterator = io_iterator_t() + defer { + if iterator != IO_OBJECT_NULL { + IOObjectRelease(iterator) + } + } + + guard + IOServiceGetMatchingServices( + defaultPort, + IOBSDNameMatching(defaultPort, 0, name), + &iterator + ) == KERN_SUCCESS + else { + return nil + } + + var candidate = IOIteratorNext(iterator) + while candidate != IO_OBJECT_NULL { + if let cfBuiltIn = IORegistryEntryCreateCFProperty( + candidate, + "IOBuiltin" as CFString, + kCFAllocatorDefault, + 0 + ) { + let isBuiltIn = cfBuiltIn.takeRetainedValue() as? Bool ?? false + if isBuiltIn == wantBuiltIn { + return candidate + } + } + IOObjectRelease(candidate) + candidate = IOIteratorNext(iterator) + } + + return nil + } + + private static func obtainMACAddress() -> Data? { + let interfaces = ["en0", "en1", "en2", "en3", "en4"] + + for interface in interfaces { + if let service = ioService(named: interface, wantBuiltIn: true), + let cfData = IORegistryEntrySearchCFProperty( + service, + kIOServicePlane, + "IOMACAddress" as CFString, + kCFAllocatorDefault, + IOOptionBits(kIORegistryIterateRecursively | kIORegistryIterateParents) + ) as? Data + { + IOObjectRelease(service) + return cfData + } + } + + return nil + } +} +#endif diff --git a/Sources/Validator/Default Verifiers/Hash/HashVerifier.swift b/Sources/Validator/Default Verifiers/Hash/HashVerifier.swift new file mode 100644 index 0000000..c56603c --- /dev/null +++ b/Sources/Validator/Default Verifiers/Hash/HashVerifier.swift @@ -0,0 +1,59 @@ +import Crypto +import Foundation + +// MARK: - Receipt Hash Verifier + +public final class HashVerifier: ReceiptHashVerifier { + private let deviceIdentifier: Data + + public init(deviceIdentifier: Data) { + self.deviceIdentifier = deviceIdentifier + } + + func computeHash( + deviceIdentifier: Data, + opaqueValue: Data, + bundleIdentifierData: Data + ) -> Data { + var sha1 = Insecure.SHA1() + sha1.update(data: deviceIdentifier) + sha1.update(data: opaqueValue) + sha1.update(data: bundleIdentifierData) + + let digest = sha1.finalize() + return Data(digest) + } + + public func verify( + expectedHash: Data, + opaqueValue: Data, + bundleIdentifier: Data + ) -> VerificationResult { + let computedHash = computeHash( + deviceIdentifier: deviceIdentifier, + opaqueValue: opaqueValue, + bundleIdentifierData: bundleIdentifier + ) + + return computedHash == expectedHash ? .valid : .invalid(HashVerificationError.hashMismatch) + } +} + +extension HashVerifier: ReceiptVerifier { + public func verify(_ receipt: any ReceiptValidatable) async -> VerificationResult { + verify( + expectedHash: receipt.receiptHash, + opaqueValue: receipt.opaqueValue, + bundleIdentifier: receipt.bundleIdentifierData + ) + } + + @_spi(Blocking) + public func verify(_ receipt: any ReceiptValidatable) -> VerificationResult { + verify( + expectedHash: receipt.receiptHash, + opaqueValue: receipt.opaqueValue, + bundleIdentifier: receipt.bundleIdentifierData + ) + } +} diff --git a/Sources/Validator/Default Verifiers/Hash/ReceiptHashVerifier.swift b/Sources/Validator/Default Verifiers/Hash/ReceiptHashVerifier.swift new file mode 100644 index 0000000..f4e455c --- /dev/null +++ b/Sources/Validator/Default Verifiers/Hash/ReceiptHashVerifier.swift @@ -0,0 +1,43 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +/// Errors that occur during receipt hash verification. +/// +/// Represents failures in validating the receipt hash, which is computed from device, app, and receipt data. +public enum HashVerificationError: Hashable, Sendable, Error { + /// The device identifier is missing and cannot be used for hash computation. + case missingDeviceIdentifier + + /// The computed hash does not match the hash stored in the receipt. + /// + /// This indicates the receipt may have been tampered with or is being validated on a different device. + case hashMismatch +} + +/// A protocol for types that verify receipt hashes. +/// +/// Conforming types compute a hash using device-specific information and verify it matches the hash +/// stored in the receipt. This ensures the receipt was issued for the specific device and app that is +/// validating it, preventing misuse of receipts across devices or apps. +/// +/// - Note: All types conforming to this protocol must be `Sendable`. +public protocol ReceiptHashVerifier: Sendable { + /// Verifies the receipt hash. + /// + /// Computes a hash using the opaque value, bundle identifier, and the device's unique identifier, + /// then compares it to the expected hash from the receipt. + /// + /// - Parameters: + /// - expectedHash: The hash value stored in the receipt to verify against. + /// - opaqueValue: The opaque value from the receipt, used in hash computation. + /// - bundleIdentifier: The bundle identifier data from the receipt, used in hash computation. + /// - Returns: A verification result indicating whether the hash matches or the error that occurred. + func verify( + expectedHash: Data, + opaqueValue: Data, + bundleIdentifier: Data + ) -> VerificationResult +} diff --git a/Sources/Validator/Default Verifiers/Meta/MetaVerifier.swift b/Sources/Validator/Default Verifiers/Meta/MetaVerifier.swift new file mode 100644 index 0000000..b318a02 --- /dev/null +++ b/Sources/Validator/Default Verifiers/Meta/MetaVerifier.swift @@ -0,0 +1,86 @@ +public final class MetaVerifier: ReceiptMetaVerifier { + public typealias AppVersionProvider = @Sendable () throws -> String + public typealias AppBundleIdentifierProvider = @Sendable () throws -> String + + let appVersionProvider: AppVersionProvider + let bundleIdentifierProvider: AppBundleIdentifierProvider + + public init( + appVersionProvider: @escaping AppVersionProvider, + bundleIdentifierProvider: @escaping AppBundleIdentifierProvider + ) { + self.appVersionProvider = appVersionProvider + self.bundleIdentifierProvider = bundleIdentifierProvider + } + + /// Creates a MetaVerifier with static expected values. + /// Use this on servers when validating receipts from clients. + public convenience init( + expectedBundleIdentifier: String, + expectedAppVersion: String + ) { + self.init( + appVersionProvider: { expectedAppVersion }, + bundleIdentifierProvider: { expectedBundleIdentifier } + ) + } + + public func verify( + bundleIdentifier: String, + versionIdentifier: String + ) -> VerificationResult { + do { + let expectedVersion = try appVersionProvider() + guard expectedVersion == versionIdentifier else { + return .invalid(MetaVerificationError.versionIdentifierMismatch) + } + + let expectedBundleIdentifier = try bundleIdentifierProvider() + guard expectedBundleIdentifier == bundleIdentifier else { + return .invalid(MetaVerificationError.bundleIdentifierMismatch) + } + + return .valid + } catch { + return .invalid(error) + } + } +} + +extension MetaVerifier: ReceiptVerifier { + public func verify(_ receipt: any ReceiptValidatable) -> VerificationResult { + return verify( + bundleIdentifier: receipt.bundleIdentifier, + versionIdentifier: receipt.versionIdentifier + ) + } +} + +#if os(iOS) || os(watchOS) || os(tvOS) || os(macOS) || targetEnvironment(macCatalyst) + +import Foundation + +// MARK: Native Providers + +let nativeAppVersionProvider: MetaVerifier.AppVersionProvider = { + #if targetEnvironment(macCatalyst) || os(macOS) + let dictKey = "CFBundleShortVersionString" + #else + let dictKey = "CFBundleVersion" + #endif + + guard let v = Bundle.main.infoDictionary?[dictKey] as? String else { + throw MetaVerificationError.bundleInfoUnavailable + } + + return v +} + +let nativeBundleIdentifierProvider: MetaVerifier.AppBundleIdentifierProvider = { + guard let bundleIdentifier = Bundle.main.bundleIdentifier else { + throw MetaVerificationError.bundleInfoUnavailable + } + + return bundleIdentifier +} +#endif diff --git a/Sources/Validator/Default Verifiers/Meta/ReceiptMetaVerifier.swift b/Sources/Validator/Default Verifiers/Meta/ReceiptMetaVerifier.swift new file mode 100644 index 0000000..b1c0a77 --- /dev/null +++ b/Sources/Validator/Default Verifiers/Meta/ReceiptMetaVerifier.swift @@ -0,0 +1,37 @@ +/// Errors that occur during receipt metadata verification. +/// +/// Represents failures in validating that the receipt belongs to the correct app and version. +public enum MetaVerificationError: Hashable, Sendable, Error { + /// The bundle identifier in the receipt does not match the expected app's bundle identifier. + /// + /// This can indicate the receipt is being used for a different app than it was issued for. + case bundleIdentifierMismatch + + /// The version identifier in the receipt does not match the expected app version. + case versionIdentifierMismatch + + /// The app's bundle information could not be retrieved from the main bundle. + case bundleInfoUnavailable +} + +/// A protocol for types that verify receipt metadata. +/// +/// Conforming types validate that the receipt's metadata (bundle identifier and version identifier) +/// matches the expected values for the app, ensuring the receipt was issued for the correct app +/// and version. +/// +/// - Note: All types conforming to this protocol must be `Sendable`. +public protocol ReceiptMetaVerifier: Sendable { + /// Verifies the receipt metadata. + /// + /// Validates that the receipt's bundle identifier and version identifier match the expected values. + /// + /// - Parameters: + /// - bundleIdentifier: The expected bundle identifier that should be in the receipt. + /// - versionIdentifier: The expected version identifier that should be in the receipt. + /// - Returns: A verification result indicating whether the metadata is correct or the error that occurred. + func verify( + bundleIdentifier: String, + versionIdentifier: String + ) -> VerificationResult +} diff --git a/Sources/Validator/Default Verifiers/Signature/ReceiptSignatureVerifier.swift b/Sources/Validator/Default Verifiers/Signature/ReceiptSignatureVerifier.swift new file mode 100644 index 0000000..a010e69 --- /dev/null +++ b/Sources/Validator/Default Verifiers/Signature/ReceiptSignatureVerifier.swift @@ -0,0 +1,60 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +import X509 + +/// The digest algorithm used to hash data before cryptographic signature verification. +/// +/// This enum specifies which hashing algorithm was used to create the signature over the receipt data. +public enum DigestAlgorithm: Sendable { + /// SHA-1 digest algorithm. + /// + /// A cryptographic hash function that produces a 160-bit hash value. Included for compatibility + /// with older receipt formats. + case sha1 + + /// SHA-256 digest algorithm. + /// + /// A cryptographic hash function that produces a 256-bit hash value. Preferred for modern receipts. + case sha256 +} + +/// Errors that occur during receipt signature verification. +/// +/// Represents failures in validating the cryptographic signature that authenticates the receipt's integrity. +public enum SignatureVerificationError: Hashable, Sendable, Error { + /// The public key extracted from the certificate is invalid, missing, or cannot be used for verification. + case invalidKey + + /// The signature is invalid, malformed, or does not cryptographically verify against the signed data using the provided key. + case invalidSignature +} + +/// A protocol for types that verify receipt signatures. +/// +/// Conforming types implement the cryptographic verification of receipt signatures using a public key. +/// They validate that the signature was created by the corresponding private key and that the signed data +/// has not been tampered with. +/// +/// - Note: All types conforming to this protocol must be `Sendable`. +public protocol ReceiptSignatureVerifier: Sendable { + /// Verifies the receipt signature asynchronously. + /// + /// Performs cryptographic signature verification using the specified public key and digest algorithm. + /// + /// - Parameters: + /// - key: The public key extracted from the leaf certificate, used to verify the signature. + /// - algorithm: The digest algorithm that was used when creating the signature. + /// - signedData: The original data that was signed to create the receipt signature. + /// - signature: The signature bytes to verify against the signed data. + /// - Returns: A verification result indicating whether the signature is valid or the error that occurred. + func verify( + key: Certificate.PublicKey, + algorithm: DigestAlgorithm, + signedData: Data, + signature: Data + ) async -> VerificationResult +} diff --git a/Sources/Validator/Default Verifiers/Signature/Sec_SignatureVerifier.swift b/Sources/Validator/Default Verifiers/Signature/Sec_SignatureVerifier.swift new file mode 100644 index 0000000..cb0dee6 --- /dev/null +++ b/Sources/Validator/Default Verifiers/Signature/Sec_SignatureVerifier.swift @@ -0,0 +1,90 @@ +#if canImport(Security) + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@preconcurrency import Security +import SwiftASN1 +import X509 + +public struct SecSignatureVerifier { + public init() {} + + public func verify( + key: Data, + algorithm: DigestAlgorithm, + signedData: Data, + signature: Data + ) -> VerificationResult { + let keyDict: [String: Any] = [ + kSecAttrKeyClass as String: kSecAttrKeyClassPublic, + kSecAttrKeyType as String: kSecAttrKeyTypeRSA, + kSecAttrKeySizeInBits as String: 2048, + ] + + guard + let publicKeySec = SecKeyCreateWithData( + key as CFData, + keyDict as CFDictionary, + nil + ) + else { + return .invalid(SignatureVerificationError.invalidKey) + } + + var umErrorCF: Unmanaged? = nil + guard + SecKeyVerifySignature( + publicKeySec, + algorithm.secKeyAlgorithm, + signedData as CFData, + signature as CFData, + &umErrorCF + ) + else { + return .invalid(SignatureVerificationError.invalidSignature) + } + + return .valid + } +} + +extension SecSignatureVerifier: ReceiptVerifier { + public func verify(_ receipt: any ReceiptValidatable) -> VerificationResult { + guard let publicKey = receipt.publicKey, + let key = try? Data(publicKey.intoBytes()) + else { + return .invalid(SignatureVerificationError.invalidKey) + } + + return verify( + key: key, + algorithm: receipt.digestAlgorithm, + signedData: receipt.digestData, + signature: receipt.signature + ) + } +} + +extension DigestAlgorithm { + fileprivate var secKeyAlgorithm: SecKeyAlgorithm { + switch self { + case .sha1: + return .rsaSignatureMessagePKCS1v15SHA1 + case .sha256: + return .rsaSignatureMessagePKCS1v15SHA256 + } + } +} + +extension DERSerializable { + func intoBytes() throws -> Data { + var serializer = DER.Serializer() + try serializer.serialize(self) + return Data(serializer.serializedBytes) + } +} +#endif diff --git a/Sources/Validator/Default Verifiers/Signature/X509_SignatureVerifier.swift b/Sources/Validator/Default Verifiers/Signature/X509_SignatureVerifier.swift new file mode 100644 index 0000000..5c277d8 --- /dev/null +++ b/Sources/Validator/Default Verifiers/Signature/X509_SignatureVerifier.swift @@ -0,0 +1,59 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +import _CryptoExtras +import Crypto +import SwiftASN1 +import X509 + +public struct SignatureVerifier: ReceiptSignatureVerifier { + public func verify( + key: Certificate.PublicKey, + algorithm: DigestAlgorithm, + signedData: Data, + signature: Data + ) -> VerificationResult { + guard let signatureAlgorithm = algorithm.signatureAlgorithm else { + return .invalid(SignatureVerificationError.invalidSignature) + } + + let isValid = key.isValidSignature( + signature, + for: signedData, + signatureAlgorithm: signatureAlgorithm + ) + + return isValid ? .valid : .invalid(SignatureVerificationError.invalidSignature) + } +} + +extension SignatureVerifier: ReceiptVerifier { + public func verify(_ receipt: any ReceiptValidatable) -> VerificationResult { + guard let leaf = receipt.certificates.first else { + return .invalid(SignatureVerificationError.invalidKey) + } + + return verify( + key: leaf.publicKey, + algorithm: receipt.digestAlgorithm, + signedData: receipt.digestData, + signature: receipt.signature + ) + } +} + +extension DigestAlgorithm { + fileprivate var signatureAlgorithm: Certificate.SignatureAlgorithm? { + switch self { + case .sha1: + return .sha1WithRSAEncryption + case .sha256: + return .sha256WithRSAEncryption + @unknown default: + return nil + } + } +} diff --git a/Sources/Validator/ReceiptValidatable.swift b/Sources/Validator/ReceiptValidatable.swift new file mode 100644 index 0000000..272fb00 --- /dev/null +++ b/Sources/Validator/ReceiptValidatable.swift @@ -0,0 +1,68 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +import X509 + +// MARK: - ReceiptValidatable + +/// A protocol for objects that contain App Store receipt data for validation. +/// +/// This protocol defines the interface for accessing and validating receipt properties required by verifiers. +public protocol ReceiptValidatable: Sendable { + /// The environment where the receipt was generated (sandbox or production). + var environment: InAppReceiptPayload.Environment { get } + + /// The version identifier of the app that generated the receipt. + var versionIdentifier: String { get } + + /// The app's bundle identifier as a string. + var bundleIdentifier: String { get } + + /// The app's bundle identifier encoded as data. + var bundleIdentifierData: Data { get } + + /// The certificate chain used to sign the receipt, parsed as `Certificate` objects. + var certificates: [Certificate] { get } + + /// The raw certificate data from the receipt. + var certificatesRaw: [Data] { get } + + /// The expected hash value computed from the receipt's opaque value, bundle identifier, and device identifier. + var receiptHash: Data { get } + + /// The opaque value from the receipt used in hash computation. + var opaqueValue: Data { get } + + /// The data that was signed to create the receipt signature. + var digestData: Data { get } + + /// The digest algorithm used to hash the signed data. + var digestAlgorithm: DigestAlgorithm { get } + + /// The signature that verifies the authenticity of the receipt. + var signature: Data { get } + + /// The date to use for validating certificate expiration and other time-dependent verification checks. + var validationTime: Date { get } +} + +extension ReceiptValidatable { + var leafCertificate: Certificate? { + certificates.first + } + + var intermediateCertificates: [Certificate] { + guard certificates.count > 1 else { + return [] + } + + return [certificates[1]] + } + + var publicKey: Certificate.PublicKey? { + leafCertificate?.publicKey + } +} diff --git a/Sources/Validator/ReceiptVerifier.swift b/Sources/Validator/ReceiptVerifier.swift new file mode 100644 index 0000000..cada721 --- /dev/null +++ b/Sources/Validator/ReceiptVerifier.swift @@ -0,0 +1,61 @@ +/// A protocol for types that verify aspects of an App Store receipt. +/// +/// Conforming types perform specific validation checks on receipts, such as certificate chain validation, +/// signature verification, hash verification, or metadata validation. Multiple verifiers can be composed +/// together to perform comprehensive receipt validation. +public protocol ReceiptVerifier: Sendable { + /// Verifies the receipt asynchronously. + /// + /// - Parameter receipt: The receipt data to verify. + /// - Returns: A verification result indicating success or failure with an associated error. + func verify(_ receipt: any ReceiptValidatable) async -> VerificationResult + + /// Verifies the receipt synchronously. + /// + /// This is a blocking variant of the async `verify(_:)` method for use in synchronous contexts. + /// + /// - Parameter receipt: The receipt data to verify. + /// - Returns: A verification result indicating success or failure with an associated error. + @_spi(Blocking) + func verify(_ receipt: any ReceiptValidatable) -> VerificationResult +} + +/// The result of a verification operation. +/// +/// Represents the outcome of a receipt verification check. Use the `isValid` or `isInvalid` properties +/// to determine the verification status. +public enum VerificationResult: Sendable { + /// The verification succeeded. + case valid + + /// The verification failed with an error. + /// + /// - Parameter error: The error that caused the verification to fail. + case invalid(any Error) + + /// A Boolean value indicating whether the verification succeeded. + /// + /// - Returns: `true` if verification succeeded, `false` otherwise. + @inlinable + public var isValid: Bool { + switch self { + case .valid: + return true + case .invalid: + return false + } + } + + /// A Boolean value indicating whether the verification failed. + /// + /// - Returns: `true` if verification failed, `false` otherwise. + @inlinable + public var isInvalid: Bool { + switch self { + case .valid: + return false + case .invalid: + return true + } + } +} diff --git a/Sources/Validator/Validator.swift b/Sources/Validator/Validator.swift new file mode 100644 index 0000000..f4f65bd --- /dev/null +++ b/Sources/Validator/Validator.swift @@ -0,0 +1,106 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +import X509 + +/// Errors that can occur during receipt validation. +/// +/// This error type represents various failures that can occur when validating App Store receipts. +public enum ReceiptValidatorError: Error, Sendable { + /// The root certificate is invalid or cannot be decoded. + case rootCertificateInvalid((any Error)?) + + /// The device identifier could not be retrieved. + case deviceIdentifierIsNotFound + + /// The receipt structure is invalid or missing required fields. + case invalidReceiptStructure +} + +// MARK: - ReceiptValidator + +/// Validates App Store receipts using a composition of verifiers. +/// +/// `ReceiptValidator` runs multiple verification checks on a receipt in parallel using task groups. +/// It composes multiple `ReceiptVerifier` instances, each responsible for a specific verification aspect. +/// If any verifier fails, validation immediately returns that failure without running remaining verifiers. +/// +/// Use the `@VerifierBuilder` result builder to compose verifiers: +/// +/// ```swift +/// let validator = ReceiptValidator { +/// X509ChainVerifier(rootCertificates: [rootCert]) +/// SignatureVerifier() +/// HashVerifier() +/// } +/// ``` +public struct ReceiptValidator: Sendable { + let verifiers: [any ReceiptVerifier] + + /// Creates a receipt validator with the specified verifiers. + /// + /// - Parameter policies: A builder closure that returns an array of verifiers to run. + /// Use the `@VerifierBuilder` syntax to compose verifiers. + public init(@VerifierBuilder policies: () -> [any ReceiptVerifier]) { + verifiers = policies() + } + + /// Validates a receipt asynchronously using all configured verifiers. + /// + /// The validation runs verifiers in parallel using task groups. If any verifier fails, + /// remaining verifiers are cancelled and the failure is immediately returned. + /// + /// - Parameter receipt: The receipt to validate. + /// - Returns: A verification result indicating whether all verifications passed or the first failure. + public func validate(_ receipt: any ReceiptValidatable) async -> VerificationResult { + await withTaskGroup(of: VerificationResult.self) { group in + for verifier in verifiers { + group.addTask { + await verifier.verify(receipt) + } + } + + for await result in group { + if case let .invalid(error) = result { + group.cancelAll() + return .invalid(error) + } + } + + return .valid + } + } +} + +extension ReceiptValidator { + /// Creates a default validator with all standard verification checks. + /// + /// This factory method creates a fully configured receipt validator that performs: + /// - X.509 certificate chain verification against the provided root certificate + /// - Cryptographic signature verification + /// - Receipt hash verification + /// - Metadata verification (bundle identifier and app version) + /// + /// - Parameters: + /// - rootCertificate: The Apple root certificate in DER-encoded format. + /// - deviceIdentifier: The device identifier data for hash verification. + /// - Returns: A configured `ReceiptValidator` instance. + /// - Throws: An error if the root certificate cannot be decoded. + public static func `default`( + rootCertificate: Certificate, + deviceIdentifier: Data + ) -> ReceiptValidator { + return ReceiptValidator { + X509ChainVerifier(rootCertificates: [rootCertificate]) + SignatureVerifier() + HashVerifier(deviceIdentifier: deviceIdentifier) + MetaVerifier( + appVersionProvider: nativeAppVersionProvider, + bundleIdentifierProvider: nativeBundleIdentifierProvider + ) + } + } +} diff --git a/Sources/Validator/VerifierBuilder.swift b/Sources/Validator/VerifierBuilder.swift new file mode 100644 index 0000000..73b66ba --- /dev/null +++ b/Sources/Validator/VerifierBuilder.swift @@ -0,0 +1,72 @@ +// MARK: - VerifierBuilder + +/// A result builder for composing receipt verifiers. +/// +/// `VerifierBuilder` is a DSL (Domain Specific Language) that enables composition +/// of `ReceiptVerifier` instances. +/// +/// Use `@VerifierBuilder` syntax when initializing a `ReceiptValidator`: +/// +/// ```swift +/// let validator = ReceiptValidator { +/// X509ChainVerifier(rootCertificates: [rootCert]) +/// SignatureVerifier() +/// if shouldVerifyHash { +/// HashVerifier() +/// } +/// } +/// ``` +@resultBuilder +public struct VerifierBuilder: Sendable {} + +extension VerifierBuilder { + public static func buildExpression(_ expression: any ReceiptVerifier) -> [any ReceiptVerifier] { + [expression] + } + + public static func buildExpression(_ expression: [any ReceiptVerifier]) -> [any ReceiptVerifier] { + expression + } + + public static func buildBlock() -> [any ReceiptVerifier] { + [] + } + + public static func buildPartialBlock(first policy: any ReceiptVerifier) -> [any ReceiptVerifier] { + [policy] + } + + public static func buildPartialBlock(first policy: [any ReceiptVerifier]) -> [any ReceiptVerifier] { + policy + } + + public static func buildPartialBlock( + accumulated policies: [any ReceiptVerifier], + next policy: any ReceiptVerifier + ) -> [any ReceiptVerifier] { + policies + [policy] + } + + public static func buildPartialBlock( + accumulated policies: [any ReceiptVerifier], + next policy: [any ReceiptVerifier] + ) -> [any ReceiptVerifier] { + policies + policy + } + + public static func buildOptional(_ policy: [any ReceiptVerifier]?) -> [any ReceiptVerifier] { + policy ?? [] + } + + public static func buildEither(first policy: [any ReceiptVerifier]) -> [any ReceiptVerifier] { + policy + } + + public static func buildEither(second policy: [any ReceiptVerifier]) -> [any ReceiptVerifier] { + policy + } + + public static func buildArray(_ policies: [[any ReceiptVerifier]]) -> [any ReceiptVerifier] { + policies.flatMap { $0 } + } +} diff --git a/TPInAppReceipt.podspec b/TPInAppReceipt.podspec deleted file mode 100644 index d556255..0000000 --- a/TPInAppReceipt.podspec +++ /dev/null @@ -1,38 +0,0 @@ -Pod::Spec.new do |s| - - s.name = "TPInAppReceipt" - s.version = "3.4.2" - s.summary = "Reading and Validating In App Purchase Receipt Locally" - s.description = "A lightweight iOS/OSX library for reading and validating Apple In App Purchase Receipt locally. Pure swift, No OpenSSL!" - - s.homepage = "https://github.com/tikhop/TPInAppReceipt" - s.license = "MIT" - s.source = { :git => "https://github.com/tikhop/TPInAppReceipt.git", :tag => "#{s.version}" } - - s.author = { "tikhop" => "hi@tikhop.com" } - - s.swift_versions = ['5.3'] - s.ios.deployment_target = '12.0' - s.osx.deployment_target = '10.13' - s.tvos.deployment_target = '12.0' - s.watchos.deployment_target = '6.2' - s.visionos.deployment_target = '1.0' - - s.requires_arc = true - - s.subspec 'Core' do |core| - core.exclude_files = "Sources/Objc/*.{swift}" - core.source_files = "Sources/*.{swift}" - core.resources = "Sources/AppleIncRootCertificate.cer", "Sources/StoreKitTestCertificate.cer" - core.dependency 'ASN1Swift', '~> 1.2.7' - end - - s.subspec 'Objc' do |objc| - objc.source_files = "Sources/Objc/*.{swift}" - objc.dependency 'TPInAppReceipt/Core' - end - - s.resource_bundles = { "TPInAppReceipt" => "Source/PrivacyInfo.xcprivacy" } - s.default_subspecs = 'Core' - -end diff --git a/Tests/AppReceiptTests.swift b/Tests/AppReceiptTests.swift new file mode 100644 index 0000000..e00b94e --- /dev/null +++ b/Tests/AppReceiptTests.swift @@ -0,0 +1,226 @@ +import Foundation +import Testing +import X509 + +@testable import TPInAppReceipt + +@Suite("AppReceipt") +struct AppReceiptTests { + let knownDeviceUUID = UUID(uuidString: "956328D9-CC6A-47F4-BE40-6953FB0AB6C7")! + + // MARK: - Decoding & Metadata + + @Test + func decodedReceiptExposesEnvironment() throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + #expect(receipt.environment == .productionSandbox) + } + + @Test + func decodedReceiptExposesBundleIdentifier() throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + #expect(!receipt.bundleIdentifier.isEmpty) + } + + @Test + func decodedReceiptExposesAppVersion() throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + #expect(!receipt.appVersion.isEmpty) + } + + @Test + func creationDateIsReasonable() throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + let year2010 = Date(timeIntervalSince1970: 1_262_304_000) + #expect(receipt.creationDate > year2010) + } + + @Test + func payloadRawDataIsNonEmpty() throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + #expect(!receipt.payloadRawData.isEmpty) + } + + @Test + func hasValidStructure() throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + #expect(receipt.hasValidStructure) + } + + @Test + func productionReceiptExposesCorrectEnvironment() throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-production") + #expect(receipt.environment == .production) + } + + @Test + func xcodeReceiptExposesCorrectEnvironment() throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-no-orig-purchase-date") + #expect(receipt.environment == .xcode) + } + + // MARK: - Validation + + @Test + func asyncValidationPassesWithCorrectCertAndDevice() async throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + let rootCertData = TestingUtility.loadRootCertificate() + let rootCert = try Certificate(derEncoded: rootCertData.bytes) + + let validator = ReceiptValidator { + X509ChainVerifier(rootCertificates: [rootCert]) + SignatureVerifier() + HashVerifier(deviceIdentifier: knownDeviceUUID.data) + } + + let result = await validator.validate(receipt) + #expect(result.isValid) + } + + @Test + func asyncValidationFailsWithWrongDevice() async throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + let rootCertData = TestingUtility.loadRootCertificate() + let rootCert = try Certificate(derEncoded: rootCertData.bytes) + let wrongDevice = Data(repeating: 0xFF, count: 16) + + let validator = ReceiptValidator { + X509ChainVerifier(rootCertificates: [rootCert]) + SignatureVerifier() + HashVerifier(deviceIdentifier: wrongDevice) + } + + let result = await validator.validate(receipt) + #expect(result.isInvalid) + } + + @Test + func asyncValidationFailsWithWrongRootCertificate() async throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + let wrongCertData = TestingUtility.loadXcodeRootCertificate() + let wrongCert = try Certificate(derEncoded: wrongCertData.bytes) + + let validator = ReceiptValidator { + X509ChainVerifier(rootCertificates: [wrongCert]) + SignatureVerifier() + HashVerifier(deviceIdentifier: knownDeviceUUID.data) + } + + let result = await validator.validate(receipt) + #expect(result.isInvalid) + } + + // MARK: - Purchase Queries (negative cases) + + @Test + func containsPurchaseReturnsFalseForUnknownProduct() throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + #expect(!receipt.containsPurchase(ofProductIdentifier: "com.nonexistent.product.xyz")) + } + + @Test + func purchasesReturnsEmptyForUnknownProduct() throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + #expect(receipt.purchases(ofProductIdentifier: "com.nonexistent.product.xyz").isEmpty) + } + + @Test + func originalTransactionIdentifierReturnsNilForUnknownProduct() throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + #expect(receipt.originalTransactionIdentifier(ofProductIdentifier: "com.nonexistent.product.xyz") == nil) + } + + @Test + func noActiveSubscriptionForUnknownProduct() throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + #expect( + !receipt.hasActiveAutoRenewableSubscription( + ofProductIdentifier: "com.nonexistent.product.xyz", + forDate: Date() + ) + ) + } + + @Test + func lastAutoRenewableReturnsNilForUnknownProduct() throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + #expect( + receipt.lastAutoRenewableSubscriptionPurchase( + ofProductIdentifier: "com.nonexistent.product.xyz" + ) == nil + ) + } + + // MARK: - Purchase Queries (positive cases via receipt-with-transaction) + + @Test + func containsPurchaseReturnsTrueForExistingProduct() throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-with-transaction") + try #require(!receipt.purchases.isEmpty, "Receipt must contain purchases for this test") + + let productId = receipt.purchases[0].productIdentifier + #expect(receipt.containsPurchase(ofProductIdentifier: productId)) + } + + @Test + func purchasesFiltersToMatchingProduct() throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-with-transaction") + try #require(!receipt.purchases.isEmpty, "Receipt must contain purchases for this test") + + let productId = receipt.purchases[0].productIdentifier + let filtered = receipt.purchases(ofProductIdentifier: productId) + + #expect(!filtered.isEmpty) + #expect(filtered.allSatisfy { $0.productIdentifier == productId }) + } + + @Test + func purchasesSortedByDateDescendingByDefault() throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-with-transaction") + try #require(!receipt.purchases.isEmpty, "Receipt must contain purchases for this test") + + let productId = receipt.purchases[0].productIdentifier + let sorted = receipt.purchases(ofProductIdentifier: productId) + + for i in 0..<(sorted.count - 1) { + #expect(sorted[i].purchaseDate >= sorted[i + 1].purchaseDate) + } + } + + @Test + func purchasesWithCustomSort() throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-with-transaction") + try #require(!receipt.purchases.isEmpty, "Receipt must contain purchases for this test") + + let productId = receipt.purchases[0].productIdentifier + let ascending = receipt.purchases(ofProductIdentifier: productId) { + $0.purchaseDate < $1.purchaseDate + } + + for i in 0..<(ascending.count - 1) { + #expect(ascending[i].purchaseDate <= ascending[i + 1].purchaseDate) + } + } + + @Test + func originalTransactionIdentifierReturnsValueForExistingProduct() throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-with-transaction") + try #require(!receipt.purchases.isEmpty, "Receipt must contain purchases for this test") + + let productId = receipt.purchases[0].productIdentifier + #expect(receipt.originalTransactionIdentifier(ofProductIdentifier: productId) != nil) + } + + @Test + func hasPurchasesReflectsPurchasePresence() throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-with-transaction") + #expect(receipt.hasPurchases) + } + + @Test + func autoRenewablePurchasesFiltersSubscriptions() throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-with-transaction") + let autoRenewable = receipt.autoRenewablePurchases + #expect(autoRenewable.allSatisfy { $0.isRenewableSubscription }) + } +} diff --git a/Tests/Assets/AppleIncRootCertificate.cer b/Tests/Assets/AppleIncRootCertificate.cer new file mode 100644 index 0000000..8a9ff24 Binary files /dev/null and b/Tests/Assets/AppleIncRootCertificate.cer differ diff --git a/Tests/Assets/StoreKitTestCertificate.cer b/Tests/Assets/StoreKitTestCertificate.cer new file mode 100644 index 0000000..5896156 Binary files /dev/null and b/Tests/Assets/StoreKitTestCertificate.cer differ diff --git a/Tests/Assets/receipt-crash b/Tests/Assets/receipt-crash new file mode 100644 index 0000000..d9a5e95 --- /dev/null +++ b/Tests/Assets/receipt-crash @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/Tests/Assets/receipt-from-known-device b/Tests/Assets/receipt-from-known-device new file mode 100644 index 0000000..4d0f2e9 --- /dev/null +++ b/Tests/Assets/receipt-from-known-device @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/Tests/Assets/receipt-legacy b/Tests/Assets/receipt-legacy new file mode 100644 index 0000000..8a4bc5f --- /dev/null +++ b/Tests/Assets/receipt-legacy @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/Tests/Assets/receipt-new b/Tests/Assets/receipt-new new file mode 100644 index 0000000..46fbd6b --- /dev/null +++ b/Tests/Assets/receipt-new @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/Tests/Assets/receipt-no-orig-purchase-date b/Tests/Assets/receipt-no-orig-purchase-date new file mode 100644 index 0000000..eede83b --- /dev/null +++ b/Tests/Assets/receipt-no-orig-purchase-date @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/Tests/Assets/receipt-production b/Tests/Assets/receipt-production new file mode 100644 index 0000000..9f90214 --- /dev/null +++ b/Tests/Assets/receipt-production @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/Tests/Assets/receipt-watch b/Tests/Assets/receipt-watch new file mode 100644 index 0000000..0f78688 --- /dev/null +++ b/Tests/Assets/receipt-watch @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/Tests/Assets/receipt-with-transaction b/Tests/Assets/receipt-with-transaction new file mode 100644 index 0000000..6b2d6e7 --- /dev/null +++ b/Tests/Assets/receipt-with-transaction @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/Tests/Blocking/AppReceiptBlockingTests.swift b/Tests/Blocking/AppReceiptBlockingTests.swift new file mode 100644 index 0000000..f0be7bb --- /dev/null +++ b/Tests/Blocking/AppReceiptBlockingTests.swift @@ -0,0 +1,25 @@ +import Foundation +import Testing + +@_spi(Blocking) @testable import TPInAppReceipt + +@Suite("AppReceipt Blocking Mode") +struct AppReceiptBlockingTests { + let knownDeviceUUID = UUID(uuidString: "956328D9-CC6A-47F4-BE40-6953FB0AB6C7")! + + @Test + func validReceiptPassesChainSignatureAndHashVerification() throws { + let receipt = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + let rootCertificate = TestingUtility.loadRootCertificate() + + let validator = ReceiptValidator { + SecChainVerifier(rootCertificates: [rootCertificate]) + SecSignatureVerifier() + HashVerifier(deviceIdentifier: knownDeviceUUID.data) + } + + let result = validator.validate_blocking(receipt) + + #expect(result.isValid) + } +} diff --git a/Tests/Core/InAppPurchaseTests.swift b/Tests/Core/InAppPurchaseTests.swift new file mode 100644 index 0000000..e3aaaae --- /dev/null +++ b/Tests/Core/InAppPurchaseTests.swift @@ -0,0 +1,116 @@ +import Foundation +import Testing + +@testable import TPInAppReceipt + +@Suite("InAppPurchase") +struct InAppPurchaseTests { + // MARK: - isRenewableSubscription + + @Test + func renewableWhenExpirationDatePresent() { + let purchase = makePurchase( + expirationDate: Date().addingTimeInterval(3600), + cancellationDate: nil + ) + #expect(purchase.isRenewableSubscription) + } + + @Test + func notRenewableWhenExpirationDateNil() { + let purchase = makePurchase(expirationDate: nil, cancellationDate: nil) + #expect(!purchase.isRenewableSubscription) + } + + // MARK: - isActiveAutoRenewableSubscription + + @Test + func activeWhenDateWithinRange() { + let now = Date() + let purchase = makePurchase( + purchaseDate: now.addingTimeInterval(-3600), + expirationDate: now.addingTimeInterval(3600), + cancellationDate: nil + ) + #expect(purchase.isActiveAutoRenewableSubscription(forDate: now)) + } + + @Test + func inactiveWhenExpired() { + let now = Date() + let purchase = makePurchase( + purchaseDate: now.addingTimeInterval(-7200), + expirationDate: now.addingTimeInterval(-3600), + cancellationDate: nil + ) + #expect(!purchase.isActiveAutoRenewableSubscription(forDate: now)) + } + + @Test + func inactiveWhenCancelled() { + let now = Date() + let purchase = makePurchase( + purchaseDate: now.addingTimeInterval(-3600), + expirationDate: now.addingTimeInterval(3600), + cancellationDate: now.addingTimeInterval(-1800) + ) + #expect(!purchase.isActiveAutoRenewableSubscription(forDate: now)) + } + + @Test + func inactiveBeforePurchaseDate() { + let now = Date() + let purchase = makePurchase( + purchaseDate: now.addingTimeInterval(3600), + expirationDate: now.addingTimeInterval(7200), + cancellationDate: nil + ) + #expect(!purchase.isActiveAutoRenewableSubscription(forDate: now)) + } + + @Test + func activeAtExactPurchaseDate() { + let now = Date() + let purchase = makePurchase( + purchaseDate: now, + expirationDate: now.addingTimeInterval(3600), + cancellationDate: nil + ) + #expect(purchase.isActiveAutoRenewableSubscription(forDate: now)) + } + + @Test + func inactiveAtExactExpirationDate() { + let now = Date() + let purchase = makePurchase( + purchaseDate: now.addingTimeInterval(-3600), + expirationDate: now, + cancellationDate: nil + ) + #expect(!purchase.isActiveAutoRenewableSubscription(forDate: now)) + } +} + +// MARK: - Helpers + +private func makePurchase( + purchaseDate: Date = Date(), + expirationDate: Date?, + cancellationDate: Date? +) -> InAppPurchase { + InAppPurchase( + productIdentifier: "com.example.subscription", + productType: .autoRenewableSubscription, + transactionIdentifier: UUID().uuidString, + originalTransactionIdentifier: UUID().uuidString, + purchaseDate: purchaseDate, + originalPurchaseDate: purchaseDate, + subscriptionExpirationDate: expirationDate, + cancellationDate: cancellationDate, + subscriptionTrialPeriod: false, + subscriptionIntroductoryPricePeriod: false, + webOrderLineItemID: nil, + promotionalOfferIdentifier: nil, + quantity: 1 + ) +} diff --git a/Tests/Decoding/AppReceiptDecoderTests.swift b/Tests/Decoding/AppReceiptDecoderTests.swift new file mode 100644 index 0000000..053f67a --- /dev/null +++ b/Tests/Decoding/AppReceiptDecoderTests.swift @@ -0,0 +1,69 @@ +import Foundation +import SwiftASN1 +import Testing + +@testable import TPInAppReceipt + +@Suite("AppReceiptDecoder") +struct AppReceiptDecoderTests { + @Test + func defaultDecoderDecodesReceipt() throws { + let receiptData = TestingUtility.readReceipt("Assets/receipt-from-known-device") + let decoder = AppReceiptDecoder.default + + let receipt = try decoder.decode(from: receiptData) + + #expect(receipt.hasValidStructure) + #expect(!receipt.bundleIdentifier.isEmpty) + } + + @Test + func staticReceiptFactoryMethod() throws { + let receiptData = TestingUtility.readReceipt("Assets/receipt-from-known-device") + + let receipt = try AppReceipt.receipt(from: receiptData) + + #expect(receipt.hasValidStructure) + #expect(!receipt.bundleIdentifier.isEmpty) + } + + @Test + func decodeThrowsOnInvalidData() throws { + let invalidData = Data("invalid receipt data".utf8) + let decoder = AppReceiptDecoder.default + + #expect(throws: Error.self) { + try decoder.decode(from: invalidData) + } + } + + @Test + func decodeThrowsOnEmptyData() throws { + let emptyData = Data() + let decoder = AppReceiptDecoder.default + + #expect(throws: Error.self) { + try decoder.decode(from: emptyData) + } + } + + @Test + func customEngineIsCalled() throws { + final class MockEngine: AppReceiptDecoder.Engine { + nonisolated(unsafe) var decodeCalled = false + + func decode(from data: Data) throws -> AppReceipt { + decodeCalled = true + return try AppReceipt(berEncoded: BER.parse(data.bytes)) + } + } + + let mockEngine = MockEngine() + let decoder = AppReceiptDecoder(engine: mockEngine) + let receiptData = TestingUtility.readReceipt("Assets/receipt-from-known-device") + + _ = try decoder.decode(from: receiptData) + + #expect(mockEngine.decodeCalled) + } +} diff --git a/Tests/Decoding/SwiftASN1DecoderTests.swift b/Tests/Decoding/SwiftASN1DecoderTests.swift new file mode 100644 index 0000000..bb2c379 --- /dev/null +++ b/Tests/Decoding/SwiftASN1DecoderTests.swift @@ -0,0 +1,49 @@ +import Foundation +import SwiftASN1 +import Testing + +@testable import TPInAppReceipt + +@Suite("SwiftASN1ReceiptDecoder") +struct SwiftASN1DecoderTests { + struct ReceiptTestCase: Sendable, CustomTestStringConvertible { + let path: String + let expectedEnvironment: InAppReceiptPayload.Environment? + + var testDescription: String { path } + } + + static let allReceipts: [ReceiptTestCase] = [ + .init(path: "Assets/receipt-from-known-device", expectedEnvironment: .productionSandbox), + .init(path: "Assets/receipt-production", expectedEnvironment: .production), + .init(path: "Assets/receipt-no-orig-purchase-date", expectedEnvironment: .xcode), + .init(path: "Assets/receipt-crash", expectedEnvironment: nil), + .init(path: "Assets/receipt-legacy", expectedEnvironment: nil), + .init(path: "Assets/receipt-new", expectedEnvironment: nil), + .init(path: "Assets/receipt-watch", expectedEnvironment: nil), + .init(path: "Assets/receipt-with-transaction", expectedEnvironment: nil), + ] + + @Test(arguments: allReceipts) + func decodeReceipt(testCase: ReceiptTestCase) throws { + let receiptData = TestingUtility.readReceipt(testCase.path) + let decoder = SwiftASN1ReceiptDecoder() + + let receipt = try decoder.decode(from: receiptData) + + #expect(receipt.hasValidStructure) + if let expected = testCase.expectedEnvironment { + #expect(receipt.environment == expected) + } + } + + @Test + func decodeThrowsOnMalformedData() throws { + let malformedData = Data([0x01, 0x02, 0x03, 0x04]) + let decoder = SwiftASN1ReceiptDecoder() + + #expect(throws: Error.self) { + try decoder.decode(from: malformedData) + } + } +} diff --git a/Tests/Extras/IntroOfferEligibilityTests.swift b/Tests/Extras/IntroOfferEligibilityTests.swift new file mode 100644 index 0000000..cefaf38 --- /dev/null +++ b/Tests/Extras/IntroOfferEligibilityTests.swift @@ -0,0 +1,178 @@ +import Foundation +import Testing + +@testable import TPInAppReceipt + +@Suite("IntroOfferEligibility") +struct IntroOfferEligibilityTests { + // MARK: - Per-Product Eligibility + + @Test + func eligibleWhenNoPurchases() { + let payload = makePayload(purchases: []) + #expect(payload.isEligibleForIntroOffer(for: "com.example.monthly")) + } + + @Test + func eligibleWhenProductNotPurchased() { + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.yearly", trial: false, intro: false) + ]) + #expect(payload.isEligibleForIntroOffer(for: "com.example.monthly")) + } + + @Test + func eligibleWhenPurchasedWithoutTrialOrIntro() { + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.monthly", trial: false, intro: false) + ]) + #expect(payload.isEligibleForIntroOffer(for: "com.example.monthly")) + } + + @Test + func notEligibleWhenRedeemedTrial() { + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.monthly", trial: true, intro: false) + ]) + #expect(!payload.isEligibleForIntroOffer(for: "com.example.monthly")) + } + + @Test + func notEligibleWhenRedeemedIntroPrice() { + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.monthly", trial: false, intro: true) + ]) + #expect(!payload.isEligibleForIntroOffer(for: "com.example.monthly")) + } + + @Test + func notEligibleWhenRedeemedBothTrialAndIntro() { + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.monthly", trial: true, intro: true) + ]) + #expect(!payload.isEligibleForIntroOffer(for: "com.example.monthly")) + } + + @Test + func eligibleWhenOtherProductUsedTrial() { + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.yearly", trial: true, intro: false) + ]) + #expect(payload.isEligibleForIntroOffer(for: "com.example.monthly")) + } + + @Test + func notEligibleWhenAnyPurchaseForProductUsedTrial() { + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.monthly", trial: false, intro: false), + makePurchase(productIdentifier: "com.example.monthly", trial: true, intro: false), + ]) + #expect(!payload.isEligibleForIntroOffer(for: "com.example.monthly")) + } + + // MARK: - Per-Group Eligibility + + @Test + func groupEligibleWhenNoPurchases() { + let payload = makePayload(purchases: []) + let group: Set = ["com.example.monthly", "com.example.yearly"] + #expect(payload.isEligibleForIntroOffer(for: group)) + } + + @Test + func groupEligibleWhenNoGroupProductPurchased() { + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.other", trial: true, intro: false) + ]) + let group: Set = ["com.example.monthly", "com.example.yearly"] + #expect(payload.isEligibleForIntroOffer(for: group)) + } + + @Test + func groupEligibleWhenGroupProductPurchasedWithoutTrialOrIntro() { + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.monthly", trial: false, intro: false) + ]) + let group: Set = ["com.example.monthly", "com.example.yearly"] + #expect(payload.isEligibleForIntroOffer(for: group)) + } + + @Test + func groupNotEligibleWhenAnyGroupProductUsedTrial() { + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.monthly", trial: true, intro: false) + ]) + let group: Set = ["com.example.monthly", "com.example.yearly"] + #expect(!payload.isEligibleForIntroOffer(for: group)) + } + + @Test + func groupNotEligibleWhenAnyGroupProductUsedIntroPrice() { + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.yearly", trial: false, intro: true) + ]) + let group: Set = ["com.example.monthly", "com.example.yearly"] + #expect(!payload.isEligibleForIntroOffer(for: group)) + } + + @Test + func groupNotEligibleWhenOneOfManyGroupProductsUsedTrial() { + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.monthly", trial: false, intro: false), + makePurchase(productIdentifier: "com.example.yearly", trial: true, intro: false), + ]) + let group: Set = ["com.example.monthly", "com.example.yearly"] + #expect(!payload.isEligibleForIntroOffer(for: group)) + } + + @Test + func groupEligibleWithMixedGroupAndNonGroupPurchases() { + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.monthly", trial: false, intro: false), + makePurchase(productIdentifier: "com.example.other", trial: true, intro: true), + ]) + let group: Set = ["com.example.monthly", "com.example.yearly"] + #expect(payload.isEligibleForIntroOffer(for: group)) + } +} + +// MARK: - Helpers + +private func makePurchase( + productIdentifier: String, + trial: Bool, + intro: Bool +) -> InAppPurchase { + InAppPurchase( + productIdentifier: productIdentifier, + productType: .autoRenewableSubscription, + transactionIdentifier: UUID().uuidString, + originalTransactionIdentifier: UUID().uuidString, + purchaseDate: Date(), + originalPurchaseDate: Date(), + subscriptionExpirationDate: nil, + cancellationDate: nil, + subscriptionTrialPeriod: trial, + subscriptionIntroductoryPricePeriod: intro, + webOrderLineItemID: nil, + promotionalOfferIdentifier: nil, + quantity: 1 + ) +} + +private func makePayload(purchases: [InAppPurchase]) -> InAppReceiptPayload { + InAppReceiptPayload( + bundleIdentifier: "com.example.app", + appVersion: "1.0", + originalAppVersion: "1.0", + originalPurchaseDate: Date(), + purchases: purchases, + expirationDate: nil, + bundleIdentifierData: Data(), + opaqueValue: Data(), + receiptHash: Data(), + creationDate: Date(), + ageRating: nil, + environment: .sandbox + ) +} diff --git a/Tests/Extras/PurchaseQueryTests.swift b/Tests/Extras/PurchaseQueryTests.swift new file mode 100644 index 0000000..bf09b54 --- /dev/null +++ b/Tests/Extras/PurchaseQueryTests.swift @@ -0,0 +1,364 @@ +import Foundation +import Testing + +@testable import TPInAppReceipt + +@Suite("PurchaseQueries") +struct PurchaseQueryTests { + // MARK: - containsPurchase + + @Test + func containsPurchaseWhenProductExists() { + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.monthly") + ]) + #expect(payload.containsPurchase(ofProductIdentifier: "com.example.monthly")) + } + + @Test + func doesNotContainPurchaseWhenProductAbsent() { + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.monthly") + ]) + #expect(!payload.containsPurchase(ofProductIdentifier: "com.example.yearly")) + } + + @Test + func doesNotContainPurchaseWhenEmpty() { + let payload = makePayload(purchases: []) + #expect(!payload.containsPurchase(ofProductIdentifier: "com.example.monthly")) + } + + // MARK: - purchases(ofProductIdentifier:) + + @Test + func filtersToMatchingProduct() { + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.monthly"), + makePurchase(productIdentifier: "com.example.yearly"), + makePurchase(productIdentifier: "com.example.monthly"), + ]) + let result = payload.purchases(ofProductIdentifier: "com.example.monthly") + #expect(result.count == 2) + #expect(result.allSatisfy { $0.productIdentifier == "com.example.monthly" }) + } + + @Test + func defaultSortIsDateDescending() { + let now = Date() + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.monthly", purchaseDate: now.addingTimeInterval(-7200)), + makePurchase(productIdentifier: "com.example.monthly", purchaseDate: now), + makePurchase(productIdentifier: "com.example.monthly", purchaseDate: now.addingTimeInterval(-3600)), + ]) + let result = payload.purchases(ofProductIdentifier: "com.example.monthly") + #expect(result.count == 3) + #expect(result[0].purchaseDate == now) + #expect(result[2].purchaseDate == now.addingTimeInterval(-7200)) + } + + @Test + func customSortAscending() { + let now = Date() + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.monthly", purchaseDate: now), + makePurchase(productIdentifier: "com.example.monthly", purchaseDate: now.addingTimeInterval(-3600)), + ]) + let result = payload.purchases(ofProductIdentifier: "com.example.monthly") { + $0.purchaseDate < $1.purchaseDate + } + #expect(result[0].purchaseDate < result[1].purchaseDate) + } + + @Test + func returnsEmptyWhenNoMatch() { + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.yearly") + ]) + #expect(payload.purchases(ofProductIdentifier: "com.example.monthly").isEmpty) + } + + // MARK: - originalTransactionIdentifier + + @Test + func returnsOriginalTransactionId() { + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.monthly", originalTxId: "orig-001") + ]) + #expect(payload.originalTransactionIdentifier(ofProductIdentifier: "com.example.monthly") == "orig-001") + } + + @Test + func returnsNilForMissingProduct() { + let payload = makePayload(purchases: []) + #expect(payload.originalTransactionIdentifier(ofProductIdentifier: "com.example.monthly") == nil) + } + + // MARK: - activeAutoRenewableSubscriptionPurchases + + @Test + func findsActiveSubscription() { + let now = Date() + let payload = makePayload(purchases: [ + makeSubscription( + productIdentifier: "com.example.monthly", + purchaseDate: now.addingTimeInterval(-3600), + expirationDate: now.addingTimeInterval(3600) + ) + ]) + let active = payload.activeAutoRenewableSubscriptionPurchases( + ofProductIdentifier: "com.example.monthly", + forDate: now + ) + #expect(active != nil) + #expect(active?.productIdentifier == "com.example.monthly") + } + + @Test + func returnsNilWhenSubscriptionExpired() { + let now = Date() + let payload = makePayload(purchases: [ + makeSubscription( + productIdentifier: "com.example.monthly", + purchaseDate: now.addingTimeInterval(-7200), + expirationDate: now.addingTimeInterval(-3600) + ) + ]) + let active = payload.activeAutoRenewableSubscriptionPurchases( + ofProductIdentifier: "com.example.monthly", + forDate: now + ) + #expect(active == nil) + } + + // MARK: - lastAutoRenewableSubscriptionPurchase + + @Test + func returnsSubscriptionWithLatestExpiration() { + let now = Date() + let payload = makePayload(purchases: [ + makeSubscription( + productIdentifier: "com.example.monthly", + purchaseDate: now.addingTimeInterval(-7200), + expirationDate: now.addingTimeInterval(-3600) + ), + makeSubscription( + productIdentifier: "com.example.monthly", + purchaseDate: now.addingTimeInterval(-3600), + expirationDate: now.addingTimeInterval(3600) + ), + ]) + let last = payload.lastAutoRenewableSubscriptionPurchase( + ofProductIdentifier: "com.example.monthly" + ) + #expect(last?.subscriptionExpirationDate == now.addingTimeInterval(3600)) + } + + @Test + func returnsNilWhenNoSubscriptions() { + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.consumable") + ]) + #expect( + payload.lastAutoRenewableSubscriptionPurchase( + ofProductIdentifier: "com.example.consumable" + ) == nil + ) + } + + // MARK: - hasActiveAutoRenewableSubscription + + @Test + func hasActiveSubscriptionWhenActive() { + let now = Date() + let payload = makePayload(purchases: [ + makeSubscription( + productIdentifier: "com.example.monthly", + purchaseDate: now.addingTimeInterval(-3600), + expirationDate: now.addingTimeInterval(3600) + ) + ]) + #expect( + payload.hasActiveAutoRenewableSubscription( + ofProductIdentifier: "com.example.monthly", + forDate: now + ) + ) + } + + @Test + func noActiveSubscriptionWhenAllExpired() { + let now = Date() + let payload = makePayload(purchases: [ + makeSubscription( + productIdentifier: "com.example.monthly", + purchaseDate: now.addingTimeInterval(-7200), + expirationDate: now.addingTimeInterval(-3600) + ) + ]) + #expect( + !payload.hasActiveAutoRenewableSubscription( + ofProductIdentifier: "com.example.monthly", + forDate: now + ) + ) + } + + // MARK: - hasPurchases / autoRenewablePurchases + + @Test + func hasPurchasesWhenNonEmpty() { + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.monthly") + ]) + #expect(payload.hasPurchases) + } + + @Test + func hasPurchasesIsFalseWhenEmpty() { + let payload = makePayload(purchases: []) + #expect(!payload.hasPurchases) + } + + @Test + func autoRenewablePurchasesFiltersCorrectly() { + let now = Date() + let payload = makePayload(purchases: [ + makePurchase(productIdentifier: "com.example.consumable"), + makeSubscription( + productIdentifier: "com.example.monthly", + purchaseDate: now.addingTimeInterval(-3600), + expirationDate: now.addingTimeInterval(3600) + ), + ]) + let autoRenewable = payload.autoRenewablePurchases + #expect(autoRenewable.count == 1) + #expect(autoRenewable[0].productIdentifier == "com.example.monthly") + } +} + +// MARK: - Helpers + +/// Wraps `InAppReceiptPayload` to expose AppReceipt-like query methods for testing +/// without needing a real PKCS#7 structure. +private struct PayloadQueryWrapper { + let payload: InAppReceiptPayload + + var purchases: [InAppPurchase] { payload.purchases } + var hasPurchases: Bool { !purchases.isEmpty } + + var autoRenewablePurchases: [InAppPurchase] { + purchases.filter { $0.isRenewableSubscription } + } + + func containsPurchase(ofProductIdentifier productIdentifier: String) -> Bool { + purchases.contains { $0.productIdentifier == productIdentifier } + } + + func purchases( + ofProductIdentifier productIdentifier: String, + sortedBy sort: ((InAppPurchase, InAppPurchase) -> Bool)? = nil + ) -> [InAppPurchase] { + let filtered = purchases.filter { $0.productIdentifier == productIdentifier } + guard let sort else { + return filtered.sorted { $0.purchaseDate > $1.purchaseDate } + } + return filtered.sorted { sort($0, $1) } + } + + func originalTransactionIdentifier(ofProductIdentifier productIdentifier: String) -> String? { + purchases(ofProductIdentifier: productIdentifier) + .first? + .originalTransactionIdentifier + } + + func activeAutoRenewableSubscriptionPurchases( + ofProductIdentifier productIdentifier: String, + forDate date: Date + ) -> InAppPurchase? { + purchases(ofProductIdentifier: productIdentifier) + .first { $0.isActiveAutoRenewableSubscription(forDate: date) } + } + + func lastAutoRenewableSubscriptionPurchase( + ofProductIdentifier productIdentifier: String + ) -> InAppPurchase? { + purchases(ofProductIdentifier: productIdentifier) + .filter { $0.subscriptionExpirationDate != nil } + .max { ($0.subscriptionExpirationDate ?? .distantPast) < ($1.subscriptionExpirationDate ?? .distantPast) } + } + + func hasActiveAutoRenewableSubscription( + ofProductIdentifier productIdentifier: String, + forDate date: Date + ) -> Bool { + activeAutoRenewableSubscriptionPurchases( + ofProductIdentifier: productIdentifier, + forDate: date + ) != nil + } +} + +private func makePayload(purchases: [InAppPurchase]) -> PayloadQueryWrapper { + PayloadQueryWrapper( + payload: InAppReceiptPayload( + bundleIdentifier: "com.example.app", + appVersion: "1.0", + originalAppVersion: "1.0", + originalPurchaseDate: Date(), + purchases: purchases, + expirationDate: nil, + bundleIdentifierData: Data(), + opaqueValue: Data(), + receiptHash: Data(), + creationDate: Date(), + ageRating: nil, + environment: .sandbox + ) + ) +} + +private func makePurchase( + productIdentifier: String, + purchaseDate: Date = Date(), + originalTxId: String = UUID().uuidString +) -> InAppPurchase { + InAppPurchase( + productIdentifier: productIdentifier, + productType: .nonConsumable, + transactionIdentifier: UUID().uuidString, + originalTransactionIdentifier: originalTxId, + purchaseDate: purchaseDate, + originalPurchaseDate: purchaseDate, + subscriptionExpirationDate: nil, + cancellationDate: nil, + subscriptionTrialPeriod: false, + subscriptionIntroductoryPricePeriod: false, + webOrderLineItemID: nil, + promotionalOfferIdentifier: nil, + quantity: 1 + ) +} + +private func makeSubscription( + productIdentifier: String, + purchaseDate: Date, + expirationDate: Date, + cancellationDate: Date? = nil +) -> InAppPurchase { + InAppPurchase( + productIdentifier: productIdentifier, + productType: .autoRenewableSubscription, + transactionIdentifier: UUID().uuidString, + originalTransactionIdentifier: UUID().uuidString, + purchaseDate: purchaseDate, + originalPurchaseDate: purchaseDate, + subscriptionExpirationDate: expirationDate, + cancellationDate: cancellationDate, + subscriptionTrialPeriod: false, + subscriptionIntroductoryPricePeriod: false, + webOrderLineItemID: nil, + promotionalOfferIdentifier: nil, + quantity: 1 + ) +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 86a148a..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,7 +0,0 @@ -import XCTest - -import TPInAppReceiptTests - -var tests = [XCTestCaseEntry]() -tests += TPInAppReceiptTests.allTests() -XCTMain(tests) diff --git a/Tests/Mocks.swift b/Tests/Mocks.swift new file mode 100644 index 0000000..98e02b5 --- /dev/null +++ b/Tests/Mocks.swift @@ -0,0 +1,24 @@ +import Foundation +import Testing + +@testable import TPInAppReceipt + +// MARK: - Mock Verifiers + +struct MockValidVerifier: ReceiptVerifier { + func verify(_: ReceiptValidatable) async -> VerificationResult { + .valid + } +} + +struct MockInvalidVerifier: ReceiptVerifier { + let error: Error + + func verify(_: ReceiptValidatable) async -> VerificationResult { + .invalid(error) + } +} + +struct MockVerificationError: Error, Equatable { + let message: String +} diff --git a/Tests/TPInAppReceiptTests/Device.swift b/Tests/TPInAppReceiptTests/Device.swift deleted file mode 100644 index 317e617..0000000 --- a/Tests/TPInAppReceiptTests/Device.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Device.swift -// TPInAppReceiptTests -// -// Created by Pavel Tikhonenko on 19.06.2020. -// Copyright © 2020 Pavel Tikhonenko. All rights reserved. -// - -import Foundation - -protocol Device -{ - var receipt: Data { get } - var uuid: String { get } -} diff --git a/Tests/TPInAppReceiptTests/PerformanceTests.swift b/Tests/TPInAppReceiptTests/PerformanceTests.swift deleted file mode 100644 index 7ba83a9..0000000 --- a/Tests/TPInAppReceiptTests/PerformanceTests.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// ASN1Tests.swift -// TPInAppReceipt -// -// Created by Pavel Tikhonenko on 19.06.20. -// Copyright © 2019-2020 Pavel Tikhonenko. All rights reserved. -// - -import XCTest -@testable import TPInAppReceipt - -class PerformanceTests: XCTestCase -{ - var receipt: InAppReceipt! - - override func setUp() - { - receipt = try! InAppReceipt(receiptData: legacyReceipt) - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testParsingPerformance() { // / 0.004 - // This is an example of a performance test case. - self.measure { - do - { - let receipt = try InAppReceipt(receiptData: legacyReceipt) - }catch{ - XCTFail("Unable to parse: \(error)") - } - } - } - - func testValidationPerformance() { // / 0.004 - // This is an example of a performance test case. - self.measure { - do - { - try receipt.validate() - }catch{ - XCTFail("Unable to verify: \(error)") - } - } - } - - func testHashValidationPerformance() { // 0.000022 // 0.000034 - // This is an example of a performance test case. - self.measure { - do - { - try receipt.verifyHash() - }catch{ - XCTFail("Unable to verify: \(error)") - } - } - } - - func testSignatureValidationPerformance() { // 0.006 // 0.002499 - // This is an example of a performance test case. - self.measure { - do - { - try receipt.verifySignature() - }catch{ - XCTFail("Unable to verify: \(error)") - } - } - } - - func testBundleValidationPerformance() { // 0.000012 // 0.000007 - // This is an example of a performance test case. - self.measure { - do - { - try receipt.verifyBundleIdentifierAndVersion() - }catch{ - XCTFail("Unable to verify: \(error)") - } - } - } - -} diff --git a/Tests/TPInAppReceiptTests/ReceiptData.swift b/Tests/TPInAppReceiptTests/ReceiptData.swift deleted file mode 100644 index f96bd26..0000000 --- a/Tests/TPInAppReceiptTests/ReceiptData.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Receipt.swift -// asn1swiftTests -// -// Created by Pavel Tikhonenko on 03.08.2020. -// - -import Foundation - -var noOriginalPurchaseDateCrashReceipt: Data = Data(base64Encoded: "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")! - -var crashReceipt: Data = Data(base64Encoded: "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")! - -var watchReceipt: Data = Data(base64Encoded: "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")! - -var newReceipt: Data = Data(base64Encoded: "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")! - -var legacyReceipt: Data = Data(base64Encoded: "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")! diff --git a/Tests/TPInAppReceiptTests/TPInAppReceiptTests.swift b/Tests/TPInAppReceiptTests/TPInAppReceiptTests.swift deleted file mode 100644 index 884a22c..0000000 --- a/Tests/TPInAppReceiptTests/TPInAppReceiptTests.swift +++ /dev/null @@ -1,27 +0,0 @@ -import XCTest -@testable import TPInAppReceipt - -final class TPInAppReceiptTests: XCTestCase { - func testCrashReceipts() { - var r = try? InAppReceipt(receiptData: noOriginalPurchaseDateCrashReceipt) - } - - func testNewReceipt() { - self.measure { - let r = try! InAppReceipt(receiptData: newReceipt) - print(r.creationDate) - } - - } - - func testLegacyReceipt() { - self.measure { - let r = try! InAppReceipt(receiptData: legacyReceipt) - } - - } - - static var allTests = [ - ("testNewReceipt", testNewReceipt) - ] -} diff --git a/Tests/TPInAppReceiptTests/XCTestManifests.swift b/Tests/TPInAppReceiptTests/XCTestManifests.swift deleted file mode 100644 index 53e1f12..0000000 --- a/Tests/TPInAppReceiptTests/XCTestManifests.swift +++ /dev/null @@ -1,9 +0,0 @@ -import XCTest - -#if !canImport(ObjectiveC) -public func allTests() -> [XCTestCaseEntry] { - return [ - testCase(TPInAppReceiptTests.allTests), - ] -} -#endif diff --git a/Tests/TestingUtility.swift b/Tests/TestingUtility.swift new file mode 100644 index 0000000..32f2d45 --- /dev/null +++ b/Tests/TestingUtility.swift @@ -0,0 +1,48 @@ +import Foundation +import SwiftASN1 +import X509 + +@testable import TPInAppReceipt + +public enum TestingUtility { + public static func readFile(_ path: String) -> String { + let absolutePath = Bundle.module.url(forResource: path, withExtension: "")! + return try! String(contentsOf: absolutePath, encoding: .utf8) + } + + public static func readBytes(_ path: String) -> Data { + let absolutePath = Bundle.module.url(forResource: path, withExtension: "")! + return try! Data(contentsOf: absolutePath) + } + + public static func readReceipt(_ path: String) -> Data { + let file = readFile(path) + return Data(base64Encoded: file)! + } + + public static func parseReceipt(_ path: String) throws -> AppReceipt { + let receiptData = TestingUtility.readReceipt(path) + let result = try BER.parse(Array(receiptData)) + return try AppReceipt(berEncoded: result) + } + + public static func certificateToData(_ certificate: Certificate) throws -> Data { + var serializer = DER.Serializer() + try serializer.serialize(certificate) + return Data(serializer.serializedBytes) + } + + public static func loadRootCertificate() -> Data { + TestingUtility.readBytes("Assets/AppleIncRootCertificate.cer") + } + + public static func loadXcodeRootCertificate() -> Data { + TestingUtility.readBytes("Assets/StoreKitTestCertificate.cer") + } +} + +extension Data { + var bytes: [UInt8] { + Array(self) + } +} diff --git a/Tests/Validation/ReceiptValidatorTests.swift b/Tests/Validation/ReceiptValidatorTests.swift new file mode 100644 index 0000000..4887e34 --- /dev/null +++ b/Tests/Validation/ReceiptValidatorTests.swift @@ -0,0 +1,44 @@ +import Foundation +import Testing + +@testable import TPInAppReceipt + +@Suite("ReceiptValidator") +struct ReceiptValidatorTests { + @Test + func validationSucceedsWhenAllVerifiersPass() async throws { + let validator = ReceiptValidator { + MockValidVerifier() + MockValidVerifier() + MockValidVerifier() + } + + let receipt = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + let result = await validator.validate(receipt) + + #expect(result.isValid) + } + + @Test + func validationFailsWhenAnyVerifierFails() async throws { + let expectedError = MockVerificationError(message: "Verification failed") + + let validator = ReceiptValidator { + MockValidVerifier() + MockInvalidVerifier(error: expectedError) + MockValidVerifier() + } + + let receipt = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + let result = await validator.validate(receipt) + + #expect(result.isInvalid) + + if case let .invalid(error) = result { + #expect(error is MockVerificationError) + if let mockError = error as? MockVerificationError { + #expect(mockError == expectedError) + } + } + } +} diff --git a/Tests/Validation/VerifierBuilderTests.swift b/Tests/Validation/VerifierBuilderTests.swift new file mode 100644 index 0000000..d3629f6 --- /dev/null +++ b/Tests/Validation/VerifierBuilderTests.swift @@ -0,0 +1,54 @@ +import Foundation +import Testing + +@testable import TPInAppReceipt + +@Suite("VerifierBuilder") +struct VerifierBuilderTests { + @Test + func emptyBuilderProducesNoVerifiers() { + let validator = ReceiptValidator {} + + #expect(validator.verifiers.count == 0) + } + + @Test + func builderPreservesOrder() { + let error1 = MockVerificationError(message: "First") + let error2 = MockVerificationError(message: "Second") + let error3 = MockVerificationError(message: "Third") + + let validator = ReceiptValidator { + MockInvalidVerifier(error: error1) + MockInvalidVerifier(error: error2) + MockInvalidVerifier(error: error3) + } + + #expect(validator.verifiers.count == 3) + let verifier1 = validator.verifiers[0] as! MockInvalidVerifier + let verifier2 = validator.verifiers[1] as! MockInvalidVerifier + let verifier3 = validator.verifiers[2] as! MockInvalidVerifier + + #expect(verifier1.error as? MockVerificationError == error1) + #expect(verifier2.error as? MockVerificationError == error2) + #expect(verifier3.error as? MockVerificationError == error3) + } + + @Test + func builderComposesConditionalAndLoopVerifiers() { + let includeOptional = true + let extras = [MockValidVerifier(), MockValidVerifier()] + + let validator = ReceiptValidator { + MockValidVerifier() + if includeOptional { + MockValidVerifier() + } + for verifier in extras { + verifier + } + } + + #expect(validator.verifiers.count == 4) + } +} diff --git a/Tests/Validation/Verifiers/ChainVerifierTests.swift b/Tests/Validation/Verifiers/ChainVerifierTests.swift new file mode 100644 index 0000000..ad63f00 --- /dev/null +++ b/Tests/Validation/Verifiers/ChainVerifierTests.swift @@ -0,0 +1,209 @@ +import Foundation +import SwiftASN1 +import Testing +import X509 + +@_spi(AsyncValidation) @testable import TPInAppReceipt + +// MARK: - Shared Test Cases + +typealias ChainVerifyFn = ( + _ leaf: Certificate, + _ intermediate: [Certificate], + _ policy: [ReceiptChainVerifierPolicy] +) async -> TPInAppReceipt.VerificationResult + +enum ChainVerifierTestCases { + static func validChain(using verify: ChainVerifyFn) async throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + let certificates = pkcs7.content.certificates + + let result = await verify( + certificates[0], + [certificates[1]], + [.appleX509Basic, .appStoreReceipt, .validationTime(pkcs7.creationDate)] + ) + + #expect(result.isValid) + } + + static func wrongRootCertificateFails(using verify: ChainVerifyFn) async throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-watch") + let certificates = pkcs7.content.certificates + + guard certificates.count >= 2 else { + Issue.record("Receipt should contain at least 2 certificates") + return + } + + let result = await verify( + certificates[0], + [certificates[1]], + [.appleX509Basic, .appStoreReceipt, .validationTime(pkcs7.creationDate)] + ) + + #expect(result.isInvalid) + } + + static func expiredCertificatesFails(using verify: ChainVerifyFn) async throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-legacy") + let certificates = pkcs7.content.certificates + + guard certificates.count >= 2 else { + Issue.record("Receipt should contain at least 2 certificates") + return + } + + let result = await verify( + certificates[0], + [certificates[1]], + [.appleX509Basic, .appStoreReceipt, .validationTime(Date())] + ) + + #expect(result.isInvalid) + } + + static func invalidLeafOIDFails(using verify: ChainVerifyFn) async throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-watch") + let certificates = pkcs7.content.certificates + + guard certificates.count >= 2 else { + Issue.record("Receipt should contain at least 2 certificates") + return + } + + // Use intermediate as leaf (wrong OID) + let result = await verify( + certificates[1], + [certificates[1]], + [.appleX509Basic, .appStoreReceipt, .validationTime(pkcs7.creationDate)] + ) + + #expect(result.isInvalid) + } + + static func invalidIntermediateOIDFails(using verify: ChainVerifyFn) async throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-watch") + let certificates = pkcs7.content.certificates + + guard certificates.count >= 2 else { + Issue.record("Receipt should contain at least 2 certificates") + return + } + + // Use leaf as intermediate — leaf cert lacks CA basic constraints + let result = await verify( + certificates[0], + [certificates[0]], + [.appleX509Basic, .validationTime(pkcs7.creationDate)] + ) + + #expect(result.isInvalid) + } +} + +// MARK: - X509ChainVerifier + +@Suite("X509ChainVerifier") +struct X509ChainVerifierTests { + @Test + func validChain() async throws { + let verifier = try X509ChainVerifier(rootCertificates: [TestingUtility.loadRootCertificate()]) + try await ChainVerifierTestCases.validChain { leaf, intermediate, policy in + await verifier.verify(leaf: leaf, intermediate: intermediate, policy: policy) + } + } + + @Test + func wrongRootCertificateFails() async throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-watch") + let wrongRoot = pkcs7.content.certificates[1] + let verifier = X509ChainVerifier(rootCertificates: [wrongRoot]) + try await ChainVerifierTestCases.wrongRootCertificateFails { leaf, intermediate, policy in + await verifier.verify(leaf: leaf, intermediate: intermediate, policy: policy) + } + } + + @Test + func expiredCertificatesFails() async throws { + let verifier = try X509ChainVerifier(rootCertificates: [TestingUtility.loadRootCertificate()]) + try await ChainVerifierTestCases.expiredCertificatesFails { leaf, intermediate, policy in + await verifier.verify(leaf: leaf, intermediate: intermediate, policy: policy) + } + } + + @Test + func invalidLeafOIDFails() async throws { + let verifier = try X509ChainVerifier(rootCertificates: [TestingUtility.loadRootCertificate()]) + try await ChainVerifierTestCases.invalidLeafOIDFails { leaf, intermediate, policy in + await verifier.verify(leaf: leaf, intermediate: intermediate, policy: policy) + } + } + + @Test + func invalidIntermediateOIDFails() async throws { + let verifier = try X509ChainVerifier(rootCertificates: [TestingUtility.loadRootCertificate()]) + try await ChainVerifierTestCases.invalidIntermediateOIDFails { leaf, intermediate, policy in + await verifier.verify(leaf: leaf, intermediate: intermediate, policy: policy) + } + } + + @Test + func invalidCertificateDataThrows() throws { + let invalidData = Data([0x00, 0x01, 0x02, 0x03]) + #expect(throws: (any Error).self) { + _ = try X509ChainVerifier(rootCertificates: [invalidData]) + } + } +} + +// MARK: - SecChainVerifier + +#if !os(Linux) +@Suite("SecChainVerifier") +struct SecChainVerifierTests { + @Test + func validChain() async throws { + let verifier = SecChainVerifier(rootCertificates: [TestingUtility.loadRootCertificate()]) + try await ChainVerifierTestCases.validChain { leaf, intermediate, policy in + verifier.verify(leaf: leaf, intermediate: intermediate, policy: policy) + } + } + + @Test + func wrongRootCertificateFails() async throws { + let verifier = SecChainVerifier(rootCertificates: []) + try await ChainVerifierTestCases.wrongRootCertificateFails { leaf, intermediate, policy in + verifier.verify(leaf: leaf, intermediate: intermediate, policy: policy) + } + } + + @Test + func expiredCertificatesFails() async throws { + let verifier = SecChainVerifier(rootCertificates: [TestingUtility.loadRootCertificate()]) + try await ChainVerifierTestCases.expiredCertificatesFails { leaf, intermediate, policy in + verifier.verify(leaf: leaf, intermediate: intermediate, policy: policy) + } + } + + @Test + func invalidLeafOIDFails() async throws { + let verifier = SecChainVerifier(rootCertificates: [TestingUtility.loadRootCertificate()]) + try await ChainVerifierTestCases.invalidLeafOIDFails { leaf, intermediate, policy in + verifier.verify(leaf: leaf, intermediate: intermediate, policy: policy) + } + } + + @Test + func invalidIntermediateOIDFails() async throws { + #if os(macOS) || targetEnvironment(macCatalyst) + // SecTrust on macOS does not enforce CA basic constraints on intermediates + #else + let verifier = SecChainVerifier(rootCertificates: [TestingUtility.loadRootCertificate()]) + try await ChainVerifierTestCases.invalidIntermediateOIDFails { leaf, intermediate, policy in + verifier.verify(leaf: leaf, intermediate: intermediate, policy: policy) + } + #endif + } +} +#endif diff --git a/Tests/Validation/Verifiers/ErrorPathTests.swift b/Tests/Validation/Verifiers/ErrorPathTests.swift new file mode 100644 index 0000000..df26f25 --- /dev/null +++ b/Tests/Validation/Verifiers/ErrorPathTests.swift @@ -0,0 +1,170 @@ +import Foundation +import SwiftASN1 +import Testing +import X509 + +@testable import TPInAppReceipt + +@Suite("Verifier Error Paths") +struct ErrorPathTests { + // MARK: - MetaVerifier + + @Test + func metaVerifierPropagatesProviderError() { + let verifier = MetaVerifier( + appVersionProvider: { throw MetaVerificationError.bundleInfoUnavailable }, + bundleIdentifierProvider: { "com.example.app" } + ) + + let result = verifier.verify( + bundleIdentifier: "com.example.app", + versionIdentifier: "1.0" + ) + + #expect(result.isInvalid) + expectError(result, equals: MetaVerificationError.bundleInfoUnavailable) + } + + @Test + func metaVerifierPropagatesBundleIdProviderError() { + let verifier = MetaVerifier( + appVersionProvider: { "1.0" }, + bundleIdentifierProvider: { throw MetaVerificationError.bundleInfoUnavailable } + ) + + let result = verifier.verify( + bundleIdentifier: "com.example.app", + versionIdentifier: "1.0" + ) + + #expect(result.isInvalid) + expectError(result, equals: MetaVerificationError.bundleInfoUnavailable) + } + + // MARK: - HashVerifier + + @Test + func hashMismatchReturnsSpecificError() { + let verifier = HashVerifier(deviceIdentifier: Data(repeating: 0x00, count: 16)) + + let result = verifier.verify( + expectedHash: Data(repeating: 0xFF, count: 20), + opaqueValue: Data(), + bundleIdentifier: Data() + ) + + #expect(result.isInvalid) + expectError(result, equals: HashVerificationError.hashMismatch) + } + + // MARK: - SignatureVerifier (via ReceiptVerifier) + + @Test + func signatureVerifierReturnsInvalidKeyWhenNoCertificates() { + let verifier = SignatureVerifier() + let receipt = MockReceipt.withNoCertificates() + + let result = verifier.verify(receipt) + + #expect(result.isInvalid) + expectError(result, equals: SignatureVerificationError.invalidKey) + } + + // MARK: - X509ChainVerifier (via ReceiptVerifier) + + @Test + func chainVerifierReturnsInvalidCertificateDataWhenNoCertificates() async throws { + let verifier = try X509ChainVerifier(rootCertificates: [TestingUtility.loadRootCertificate()]) + let receipt = MockReceipt.withNoCertificates() + + let result = await verifier.verify(receipt) + + #expect(result.isInvalid) + expectError(result, equals: ChainVerificationError.invalidCertificateData) + } + + @Test + func chainVerifierReturnsInvalidCertificateDataWhenSingleCertificate() async throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + let leaf = pkcs7.content.certificates[0] + let verifier = try X509ChainVerifier(rootCertificates: [TestingUtility.loadRootCertificate()]) + let receipt = MockReceipt.withCertificates([leaf]) + + let result = await verifier.verify(receipt) + + #expect(result.isInvalid) + expectError(result, equals: ChainVerificationError.invalidCertificateData) + } +} + +// MARK: - Assertion Helpers + +private func expectError( + _ result: TPInAppReceipt.VerificationResult, + equals expected: E, + sourceLocation: SourceLocation = #_sourceLocation +) { + guard case let .invalid(error) = result else { + Issue.record("Expected .invalid but got .valid", sourceLocation: sourceLocation) + return + } + guard let typed = error as? E else { + Issue.record( + "Expected \(E.self) but got \(type(of: error)): \(error)", + sourceLocation: sourceLocation + ) + return + } + #expect(typed == expected, sourceLocation: sourceLocation) +} + +// MARK: - MockReceipt + +private struct MockReceipt: ReceiptValidatable { + var environment: InAppReceiptPayload.Environment + var versionIdentifier: String + var bundleIdentifier: String + var bundleIdentifierData: Data + var certificates: [Certificate] + var certificatesRaw: [Data] + var receiptHash: Data + var opaqueValue: Data + var digestData: Data + var digestAlgorithm: DigestAlgorithm + var signature: Data + var validationTime: Date + + static func withNoCertificates() -> MockReceipt { + MockReceipt( + environment: .sandbox, + versionIdentifier: "1.0", + bundleIdentifier: "com.example.app", + bundleIdentifierData: Data(), + certificates: [], + certificatesRaw: [], + receiptHash: Data(), + opaqueValue: Data(), + digestData: Data(), + digestAlgorithm: .sha1, + signature: Data(), + validationTime: Date() + ) + } + + static func withCertificates(_ certs: [Certificate]) -> MockReceipt { + MockReceipt( + environment: .sandbox, + versionIdentifier: "1.0", + bundleIdentifier: "com.example.app", + bundleIdentifierData: Data(), + certificates: certs, + certificatesRaw: [], + receiptHash: Data(), + opaqueValue: Data(), + digestData: Data(), + digestAlgorithm: .sha1, + signature: Data(), + validationTime: Date() + ) + } +} diff --git a/Tests/Validation/Verifiers/HashVerifierTests.swift b/Tests/Validation/Verifiers/HashVerifierTests.swift new file mode 100644 index 0000000..4298ba4 --- /dev/null +++ b/Tests/Validation/Verifiers/HashVerifierTests.swift @@ -0,0 +1,41 @@ +import Foundation +import SwiftASN1 +import Testing + +@testable import TPInAppReceipt + +@Suite("HashVerifier") +struct HashVerifierTests { + let knownDeviceUUID = UUID(uuidString: "956328D9-CC6A-47F4-BE40-6953FB0AB6C7")! + + @Test + func verifyHashWithKnownDeviceReceipt() async throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + let payload = pkcs7.content.encapContentInfo.eContent! + + let verifier = HashVerifier(deviceIdentifier: knownDeviceUUID.data) + let result = verifier.verify( + expectedHash: payload.receiptHash, + opaqueValue: payload.opaqueValue, + bundleIdentifier: payload.bundleIdentifierData + ) + + #expect(result.isValid) + } + + @Test + func verificationFailsWithWrongDeviceIdentifier() async throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + let payload = pkcs7.content.encapContentInfo.eContent! + + let wrongUUID = UUID(uuidString: "00000000-0000-0000-0000-000000000000")! + let verifier = HashVerifier(deviceIdentifier: wrongUUID.data) + let result = verifier.verify( + expectedHash: payload.receiptHash, + opaqueValue: payload.opaqueValue, + bundleIdentifier: payload.bundleIdentifierData + ) + + #expect(result.isInvalid) + } +} diff --git a/Tests/Validation/Verifiers/MetaVerifierTests.swift b/Tests/Validation/Verifiers/MetaVerifierTests.swift new file mode 100644 index 0000000..d981994 --- /dev/null +++ b/Tests/Validation/Verifiers/MetaVerifierTests.swift @@ -0,0 +1,112 @@ +import Foundation +import Testing + +@_spi(Blocking) @testable import TPInAppReceipt + +@Suite("MetaVerifier") +struct MetaVerifierTests { + // MARK: - Valid Verification + + @Test + func validVerificationWithMatchingValues() { + let verifier = MetaVerifier( + expectedBundleIdentifier: "com.example.app", + expectedAppVersion: "1.0.0" + ) + + let result = verifier.verify( + bundleIdentifier: "com.example.app", + versionIdentifier: "1.0.0" + ) + + #expect(result.isValid) + } + + // MARK: - Invalid Verification + + @Test + func mismatchedBundleIdentifierFails() { + let verifier = MetaVerifier( + expectedBundleIdentifier: "com.example.app", + expectedAppVersion: "1.0.0" + ) + + let result = verifier.verify( + bundleIdentifier: "com.different.app", + versionIdentifier: "1.0.0" + ) + + #expect(result.isInvalid) + if case let .invalid(error) = result { + #expect(error is MetaVerificationError) + if let metaError = error as? MetaVerificationError { + #expect(metaError == .bundleIdentifierMismatch) + } + } + } + + @Test + func mismatchedVersionFails() { + let verifier = MetaVerifier( + expectedBundleIdentifier: "com.example.app", + expectedAppVersion: "1.0.0" + ) + + let result = verifier.verify( + bundleIdentifier: "com.example.app", + versionIdentifier: "2.0.0" + ) + + #expect(result.isInvalid) + if case let .invalid(error) = result { + #expect(error is MetaVerificationError) + if let metaError = error as? MetaVerificationError { + #expect(metaError == .versionIdentifierMismatch) + } + } + } + + @Test + func caseSensitiveBundleIdentifierFails() { + let verifier = MetaVerifier( + expectedBundleIdentifier: "com.example.app", + expectedAppVersion: "1.0.0" + ) + + let result = verifier.verify( + bundleIdentifier: "com.Example.App", + versionIdentifier: "1.0.0" + ) + + #expect(result.isInvalid) + } + + // MARK: - Provider-Based Initialization + + @Test + func providerBasedInitializerWorks() { + nonisolated(unsafe) var currentVersion = "1.0.0" + nonisolated(unsafe) var currentBundleId = "com.example.app" + + let verifier = MetaVerifier( + appVersionProvider: { currentVersion }, + bundleIdentifierProvider: { currentBundleId } + ) + + let result1 = verifier.verify( + bundleIdentifier: "com.example.app", + versionIdentifier: "1.0.0" + ) + #expect(result1.isValid) + + // Change expected values to verify providers return dynamic values + currentVersion = "2.0.0" + currentBundleId = "com.different.app" + + let result2 = verifier.verify( + bundleIdentifier: "com.example.app", + versionIdentifier: "1.0.0" + ) + #expect(result2.isInvalid) + } +} diff --git a/Tests/Validation/Verifiers/SignatureVerifierTests.swift b/Tests/Validation/Verifiers/SignatureVerifierTests.swift new file mode 100644 index 0000000..90459f1 --- /dev/null +++ b/Tests/Validation/Verifiers/SignatureVerifierTests.swift @@ -0,0 +1,198 @@ +import Foundation +import SwiftASN1 +import Testing +import X509 + +@testable import TPInAppReceipt + +// MARK: - Shared Test Cases + +enum SignatureVerifierTestCases { + static func validSignature( + using verify: (AppReceipt) -> TPInAppReceipt.VerificationResult + ) throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + let result = verify(pkcs7) + #expect(result.isValid) + } + + static func corruptedSignatureFails( + using verify: (_ signedData: Data, _ signature: Data) -> TPInAppReceipt.VerificationResult + ) throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + var corrupted = pkcs7.signature + if corrupted.count > 10 { + corrupted[5] ^= 0xFF + corrupted[10] ^= 0xFF + } + let result = verify(pkcs7.digestData, corrupted) + #expect(result.isInvalid) + } + + static func corruptedSignedDataFails( + using verify: (_ signedData: Data, _ signature: Data) -> TPInAppReceipt.VerificationResult + ) throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + var corrupted = pkcs7.digestData + if corrupted.count > 10 { + corrupted[5] ^= 0xFF + corrupted[10] ^= 0xFF + } + let result = verify(corrupted, pkcs7.signature) + #expect(result.isInvalid) + } + + static func emptySignatureFails( + using verify: (_ signedData: Data, _ signature: Data) -> TPInAppReceipt.VerificationResult + ) throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + let result = verify(pkcs7.digestData, Data()) + #expect(result.isInvalid) + } + + static func emptySignedDataFails( + using verify: (_ signedData: Data, _ signature: Data) -> TPInAppReceipt.VerificationResult + ) throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + let result = verify(Data(), pkcs7.signature) + #expect(result.isInvalid) + } +} + +// MARK: - X509 SignatureVerifier + +@Suite("SignatureVerifier") +struct SignatureVerifierTests { + let verifier = SignatureVerifier() + + @Test + func validSignature() throws { + try SignatureVerifierTestCases.validSignature { receipt in + verifier.verify(receipt) + } + } + + @Test + func corruptedSignatureFails() throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + try SignatureVerifierTestCases.corruptedSignatureFails { signedData, signature in + verifier.verify( + key: pkcs7.publicKey!, + algorithm: pkcs7.digestAlgorithm, + signedData: signedData, + signature: signature + ) + } + } + + @Test + func corruptedSignedDataFails() throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + try SignatureVerifierTestCases.corruptedSignedDataFails { signedData, signature in + verifier.verify( + key: pkcs7.publicKey!, + algorithm: pkcs7.digestAlgorithm, + signedData: signedData, + signature: signature + ) + } + } + + @Test + func emptySignatureFails() throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + try SignatureVerifierTestCases.emptySignatureFails { signedData, signature in + verifier.verify( + key: pkcs7.publicKey!, + algorithm: pkcs7.digestAlgorithm, + signedData: signedData, + signature: signature + ) + } + } + + @Test + func emptySignedDataFails() throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + try SignatureVerifierTestCases.emptySignedDataFails { signedData, signature in + verifier.verify( + key: pkcs7.publicKey!, + algorithm: pkcs7.digestAlgorithm, + signedData: signedData, + signature: signature + ) + } + } +} + +// MARK: - Security Framework SignatureVerifier + +#if !os(Linux) +@Suite("SecSignatureVerifier") +struct SecSignatureVerifierTests { + let verifier = SecSignatureVerifier() + + @Test + func validSignature() throws { + try SignatureVerifierTestCases.validSignature { receipt in + verifier.verify(receipt) + } + } + + @Test + func corruptedSignatureFails() throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + let keyData = try Data(pkcs7.publicKey!.intoBytes()) + try SignatureVerifierTestCases.corruptedSignatureFails { signedData, signature in + verifier.verify( + key: keyData, + algorithm: pkcs7.digestAlgorithm, + signedData: signedData, + signature: signature + ) + } + } + + @Test + func corruptedSignedDataFails() throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + let keyData = try Data(pkcs7.publicKey!.intoBytes()) + try SignatureVerifierTestCases.corruptedSignedDataFails { signedData, signature in + verifier.verify( + key: keyData, + algorithm: pkcs7.digestAlgorithm, + signedData: signedData, + signature: signature + ) + } + } + + @Test + func emptySignatureFails() throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + let keyData = try Data(pkcs7.publicKey!.intoBytes()) + try SignatureVerifierTestCases.emptySignatureFails { signedData, signature in + verifier.verify( + key: keyData, + algorithm: pkcs7.digestAlgorithm, + signedData: signedData, + signature: signature + ) + } + } + + @Test + func emptySignedDataFails() throws { + let pkcs7 = try TestingUtility.parseReceipt("Assets/receipt-from-known-device") + let keyData = try Data(pkcs7.publicKey!.intoBytes()) + try SignatureVerifierTestCases.emptySignedDataFails { signedData, signature in + verifier.verify( + key: keyData, + algorithm: pkcs7.digestAlgorithm, + signedData: signedData, + signature: signature + ) + } + } +} +#endif