From b63fe4cc3f6e7079a3067d1e9e4e8efecabafef8 Mon Sep 17 00:00:00 2001 From: Pavel Tikhonenko Date: Wed, 26 Jun 2019 18:10:36 +0300 Subject: [PATCH] Create a separate pod for validation with openssl --- TPInAppReceipt.podspec | 2 +- TPInAppReceipt.xcodeproj/project.pbxproj | 13 +- .../OpenSSL/InAppReceipt+OpenSSL.swift | 156 ++++++++++++++++++ TPInAppReceiptValidation.podspec | 67 ++++++++ 4 files changed, 234 insertions(+), 4 deletions(-) create mode 100644 TPInAppReceipt/OpenSSL/InAppReceipt+OpenSSL.swift create mode 100644 TPInAppReceiptValidation.podspec diff --git a/TPInAppReceipt.podspec b/TPInAppReceipt.podspec index 2f6c4eb..32aae51 100644 --- a/TPInAppReceipt.podspec +++ b/TPInAppReceipt.podspec @@ -38,7 +38,7 @@ s.source = { :git => "https://github.com/tikhop/TPInAppReceipt.git", :tag # Not including the public_header_files will make all headers public. # -s.source_files = "TPInAppReceipt/Source/*.{swift}, "Vendor/CryptoSwift/*.{swift}" +s.source_files = "TPInAppReceipt/Source/*.{swift}, Vendor/CryptoSwift/*.{swift}" # ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # diff --git a/TPInAppReceipt.xcodeproj/project.pbxproj b/TPInAppReceipt.xcodeproj/project.pbxproj index 627a2c6..2754bc3 100644 --- a/TPInAppReceipt.xcodeproj/project.pbxproj +++ b/TPInAppReceipt.xcodeproj/project.pbxproj @@ -130,6 +130,9 @@ 476C663E22C1DAC000D973F9 /* Array+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = ""; }; 476C664122C1DB6900D973F9 /* Bit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bit.swift; sourceTree = ""; }; 476C664422C1DBC300D973F9 /* Generics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Generics.swift; sourceTree = ""; }; + 476C664C22C3BE5400D973F9 /* InAppReceipt+OpenSSL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InAppReceipt+OpenSSL.swift"; sourceTree = ""; }; + 476C664F22C3BF6B00D973F9 /* TPInAppReceiptValidation.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = TPInAppReceiptValidation.podspec; sourceTree = ""; }; + 476C665022C3C12900D973F9 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 479DC54C1DA0B8BB00D9A5E3 /* Date+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = ""; }; 47D213391E4738AB00994B00 /* Initialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Initialization.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -174,6 +177,8 @@ isa = PBXGroup; children = ( 47209ACD1D9E92CC0010BCDE /* TPInAppReceipt.podspec */, + 476C664F22C3BF6B00D973F9 /* TPInAppReceiptValidation.podspec */, + 476C665022C3C12900D973F9 /* LICENSE */, 47209A031D9E0FD60010BCDE /* TPInAppReceipt */, 47209A0E1D9E0FD60010BCDE /* TPInAppReceiptTests */, 476C64C422BFC76000D973F9 /* Example */, @@ -223,7 +228,7 @@ 47209ABB1D9E111C0010BCDE /* Source */ = { isa = PBXGroup; children = ( - 476C664922C3BC0100D973F9 /* OpenSSL */, + 476C664B22C3BE4300D973F9 /* OpenSSL */, 47209ABE1D9E111C0010BCDE /* InAppReceipt.swift */, 47589AEA1E32D6BB0047DFCD /* InAppReceiptPayload.swift */, 47589AE81E31B2F90047DFCD /* InAppPurchase.swift */, @@ -307,12 +312,14 @@ name = Resources; sourceTree = ""; }; - 476C664922C3BC0100D973F9 /* OpenSSL */ = { + 476C664B22C3BE4300D973F9 /* OpenSSL */ = { isa = PBXGroup; children = ( + 476C664C22C3BE5400D973F9 /* InAppReceipt+OpenSSL.swift */, ); name = OpenSSL; - sourceTree = ""; + path = TPInAppReceipt/OpenSSL; + sourceTree = SOURCE_ROOT; }; /* End PBXGroup section */ diff --git a/TPInAppReceipt/OpenSSL/InAppReceipt+OpenSSL.swift b/TPInAppReceipt/OpenSSL/InAppReceipt+OpenSSL.swift new file mode 100644 index 0000000..42de8fd --- /dev/null +++ b/TPInAppReceipt/OpenSSL/InAppReceipt+OpenSSL.swift @@ -0,0 +1,156 @@ +// +// InAppReceipt+OpenSSL.swift +// TPInAppReceipt +// +// Created by Pavel Tikhonenko on 26/06/2019. +// Copyright © 2019 Pavel Tikhonenko. All rights reserved. +// + +import Foundation +import openssl + +/// A InAppReceipt extension helps to validate the receipt +public extension InAppReceipt +{ + /// Verify receipt in a complex way. + /// Validate signature and hash + /// + /// - throws: An error in the InAppReceipt domain, if verification can't be completed + public func verify() throws + { + try verifySignature() + try verifyHash() + } + + /// Verify only signature + /// + /// - throws: An error in the InAppReceipt domain, if verification can't be completed + public func verifySignature() throws + { + try pkcs7Container.verifySignature() + } + + /// Verify only hash + /// Should be equal to `receiptHash` value + /// + /// - throws: An error in the InAppReceipt domain, if verification fails + public func verifyHash() throws + { + if (computedHashData != receiptHash) + { + throw IARError.validationFailed(reason: .hashValidation) + } + } + + /// Computed SHA-1 hash, used to validate the receipt. + internal var computedHashData: Data + { + let uuidData = DeviceGUIDRetriever.guid() + let opaqueData = opaqueValue + let bundleIdData = bundleIdentifierData + + var hash: Array! + var sha1 = SHA1() + hash = try! sha1.update(withBytes: uuidData.bytes) + hash = try! sha1.update(withBytes: opaqueData.bytes) + hash = sha1.calculate(for: bundleIdData.bytes) + + return Data(bytes: &hash, count: hash.count) + } +} + +/// A PKCS7Wrapper extension helps to validate the receipt's signature +extension PKCS7Wrapper +{ + /// Verify signature inside pkcs7 container + /// + /// - throws: An error in the InAppReceipt domain, if verification can't be completed + func verifySignature() throws + { + + var p: UnsafePointer? = UnsafePointer(rawBuffer.baseAddress) + + guard let receiptPKCS7 = d2i_PKCS7(nil, &p, rawBuffer.count) else + { + throw IARError.initializationFailed(reason: .pkcs7ParsingError) + } + + defer { + PKCS7_free(receiptPKCS7) + } + + try checkSignatureExistance(pkcs7: receiptPKCS7) + let appleCertificate = try appleCertificateData() + try verifySignature(pkcs7: receiptPKCS7, withCertificateData: appleCertificate) + } + + /// Verify signature inside pkcs7 container using openssl library + /// + /// - throws: An error in the InAppReceipt domain, if verification can't be completed + fileprivate func verifySignature(pkcs7: UnsafeMutablePointer, withCertificateData data: Data) throws + { + let verified: Int32 = 1 + + let appleRootBIO = BIO_new(BIO_s_mem()) + + var appleRootBytes = [UInt8](repeating:0, count:data.count) + data.copyBytes(to: &appleRootBytes, count: data.count) + BIO_write(appleRootBIO, appleRootBytes, Int32(data.count)) + + let appleRootX509 = d2i_X509_bio(appleRootBIO, nil) + let store = X509_STORE_new() + + X509_STORE_add_cert(store, appleRootX509) + OpenSSL_add_all_digests() + + let result = PKCS7_verify(pkcs7, nil, store, nil, nil, 0) + + BIO_free(appleRootBIO) + X509_STORE_free(store) + EVP_cleanup() + + if verified != result + { + throw IARError.validationFailed(reason: .signatureValidation(.invalidSignature)) + } + } + + /// Verifies existance of the signature inside pkcs7 container + /// + /// - throws: An error in the InAppReceipt domain, if verification can't be completed + fileprivate func checkSignatureExistance(pkcs7: UnsafeMutablePointer) throws + { + if OBJ_obj2nid(pkcs7.pointee.type) != NID_pkcs7_signed + { + throw IARError.validationFailed(reason: .signatureValidation(.receiptIsNotSigned)) + } + + if OBJ_obj2nid(pkcs7.pointee.d.sign.pointee.contents.pointee.type) != NID_pkcs7_data + { + throw IARError.validationFailed(reason: .signatureValidation(.receiptSignedDataNotFound)) + } + } + + /// Load 'AppleIncRootCertificate' file and create 'Data' using content of the file + /// + /// - Returns: 'Data' object that represents Apple Root Certificate + /// - throws: An error if receipt file not found or 'Data' can't be created + fileprivate func appleCertificateData() throws -> Data + { + guard let appleRootURL = Bundle.init(for: type(of: self)).url(forResource: "AppleIncRootCertificate", withExtension: "cer") else + { + throw IARError.validationFailed(reason: .signatureValidation(.appleIncRootCertificateNotFound)) + } + + let appleRootData = try Data(contentsOf: appleRootURL) + + if appleRootData.count == 0 + { + throw IARError.validationFailed(reason: .signatureValidation(.unableToLoadAppleIncRootCertificate)) + } + + return appleRootData + } +} + + diff --git a/TPInAppReceiptValidation.podspec b/TPInAppReceiptValidation.podspec new file mode 100644 index 0000000..fa104aa --- /dev/null +++ b/TPInAppReceiptValidation.podspec @@ -0,0 +1,67 @@ +Pod::Spec.new do |s| + +s.name = "TPInAppReceiptValidation" +s.version = "1.2.3" +s.summary = "Decode Apple Store Receipt and make it easy to read it and validate using OpenSSL" + +s.description = "The library provides transparent way to decode and validate Apple Store Receipt. Pure swift, OpenSSL!" + +s.homepage = "http://tikhop.com" + +s.license = "MIT" +# s.license = { :type => "MIT", :file => "FILE_LICENSE" } + +s.author = { "Pavel Tikhonenko" => "hi@tikhop.com" } + +# ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # +# +# If this Pod runs only on iOS or OS X, then specify the platform and +# the deployment target. You can optionally include the target after the platform. +# + +s.ios.deployment_target = '8.2' +s.osx.deployment_target = '10.11' + +# ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # +# +# Specify the location from where the source should be retrieved. +# Supports git, hg, bzr, svn and HTTP. +# + +s.source = { :git => "https://github.com/tikhop/TPInAppReceipt.git", + :tag => "'Validation-' + s.version.to_s" } + +# ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # +# +# CocoaPods is smart about how it includes source code. For source files +# giving a folder will include any swift, h, m, mm, c & cpp files. +# For header files it will include any header in the folder. +# Not including the public_header_files will make all headers public. +# + +s.source_files = "TPInAppReceipt/Source/*.{swift}, Vendor/CryptoSwift/*.{swift}, TPInAppReceipt/OpenSSL/*.{swift}" + +# ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # + +s.swift_version = '5.0' + +s.ios.deployment_target = '9.0' +s.osx.deployment_target = '10.10' + +s.resources = "TPInAppReceipt/AppleIncRootCertificate.cer" + + +# ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # + +s.dependency 'TPInAppReceipt', '~> 2.0.0' +s.dependency 'OpenSSL-Universal', '~> 1.0.2.18' + +# ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # +# +# If your library depends on compiler flags you can set them in the xcconfig hash +# where they will only apply to your library. If you depend on other Podspecs +# you can include multiple dependencies to ensure it works. + +# s.requires_arc = true + +end