362 lines
12 KiB
Swift
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
|