Files
2024-04-28 00:56:18 +02:00

362 lines
12 KiB
Swift

//
// Validation.swift
// TPInAppReceipt
//
// Created by Pavel Tikhonenko on 19/01/17.
// Copyright © 2017-2021 Pavel Tikhonenko. All rights reserved.
//
#if canImport(UIKit)
import UIKit
#endif
#if canImport(WatchKit)
import WatchKit
#endif
#if canImport(Cocoa)
import Cocoa
import IOKit
#endif
import CommonCrypto
/// 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()
}
/// 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)
}
}
/// 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()
}
/// 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()
}
/// 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))
}
}
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))
}
}
}
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<CFError>? = 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))
}
}
/// 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)
}
}
fileprivate func guid() -> Data
{
#if targetEnvironment(macCatalyst) || os(macOS)
if let guid = getMacAddress()
{
return guid
}else{
assertionFailure("Failed to retrieve guid")
}
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
}
#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
}
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
}
#endif