diff --git a/WultraMobileTokenSDK.xcodeproj/project.pbxproj b/WultraMobileTokenSDK.xcodeproj/project.pbxproj index f77c207..e3713e8 100644 --- a/WultraMobileTokenSDK.xcodeproj/project.pbxproj +++ b/WultraMobileTokenSDK.xcodeproj/project.pbxproj @@ -66,6 +66,22 @@ DCD8B336246C1BAF00385F02 /* WMTRejectionReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD8B335246C1BAF00385F02 /* WMTRejectionReason.swift */; }; DCE660D124CEBECA00870E53 /* IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE660D024CEBECA00870E53 /* IntegrationTests.swift */; }; DCE660D324CEF56400870E53 /* IntegrationProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE660D224CEF56400870E53 /* IntegrationProxy.swift */; }; + DCE6D5742CF5F46000865D6E /* WMTSignatureAPNSEnvironmentDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE6D5732CF5F46000865D6E /* WMTSignatureAPNSEnvironmentDetector.swift */; }; + DCE6D5772CF5F5D500865D6E /* MachOReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE6D5762CF5F5D500865D6E /* MachOReader.swift */; }; + DCE6D5792CF5F5E400865D6E /* Entitlements.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE6D5782CF5F5E400865D6E /* Entitlements.swift */; }; + DCE6D5872CF5F63100865D6E /* X509PublicKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE6D5852CF5F63100865D6E /* X509PublicKey.swift */; }; + DCE6D5882CF5F63100865D6E /* X509Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE6D5822CF5F63100865D6E /* X509Extension.swift */; }; + DCE6D5892CF5F63100865D6E /* ASN1DistinguishedNames.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE6D57B2CF5F63100865D6E /* ASN1DistinguishedNames.swift */; }; + DCE6D58A2CF5F63100865D6E /* X509ExtensionClasses.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE6D5842CF5F63100865D6E /* X509ExtensionClasses.swift */; }; + DCE6D58B2CF5F63100865D6E /* ASN1Decoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE6D57A2CF5F63100865D6E /* ASN1Decoder.swift */; }; + DCE6D58C2CF5F63100865D6E /* ASN1Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE6D57E2CF5F63100865D6E /* ASN1Object.swift */; }; + DCE6D58D2CF5F63100865D6E /* PKCS7.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE6D5802CF5F63100865D6E /* PKCS7.swift */; }; + DCE6D58E2CF5F63100865D6E /* OID.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE6D57F2CF5F63100865D6E /* OID.swift */; }; + DCE6D58F2CF5F63100865D6E /* X509Certificate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE6D5812CF5F63100865D6E /* X509Certificate.swift */; }; + DCE6D5902CF5F63100865D6E /* ASN1Encoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE6D57C2CF5F63100865D6E /* ASN1Encoder.swift */; }; + DCE6D5912CF5F63100865D6E /* ASN1Identifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE6D57D2CF5F63100865D6E /* ASN1Identifier.swift */; }; + DCE6D5922CF5F63100865D6E /* X509ExtensionAltName.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE6D5832CF5F63100865D6E /* X509ExtensionAltName.swift */; }; + DCE6D5942CF5F65200865D6E /* BinaryReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE6D5932CF5F65200865D6E /* BinaryReader.swift */; }; EA294F3D29F6A07A00A0494E /* WMTOperationUIData.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA294F3C29F6A07A00A0494E /* WMTOperationUIData.swift */; }; EA44366A29F9294600DDEC1C /* WMTPostApprovaScreenReview.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA44366929F9294600DDEC1C /* WMTPostApprovaScreenReview.swift */; }; EA44366C29F9297100DDEC1C /* WMTPostApprovaScreenRedirect.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA44366B29F9297100DDEC1C /* WMTPostApprovaScreenRedirect.swift */; }; @@ -156,6 +172,22 @@ DCD8B335246C1BAF00385F02 /* WMTRejectionReason.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTRejectionReason.swift; sourceTree = ""; }; DCE660D024CEBECA00870E53 /* IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = ""; }; DCE660D224CEF56400870E53 /* IntegrationProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationProxy.swift; sourceTree = ""; }; + DCE6D5732CF5F46000865D6E /* WMTSignatureAPNSEnvironmentDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTSignatureAPNSEnvironmentDetector.swift; sourceTree = ""; }; + DCE6D5762CF5F5D500865D6E /* MachOReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MachOReader.swift; sourceTree = ""; }; + DCE6D5782CF5F5E400865D6E /* Entitlements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Entitlements.swift; sourceTree = ""; }; + DCE6D57A2CF5F63100865D6E /* ASN1Decoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASN1Decoder.swift; sourceTree = ""; }; + DCE6D57B2CF5F63100865D6E /* ASN1DistinguishedNames.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASN1DistinguishedNames.swift; sourceTree = ""; }; + DCE6D57C2CF5F63100865D6E /* ASN1Encoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASN1Encoder.swift; sourceTree = ""; }; + DCE6D57D2CF5F63100865D6E /* ASN1Identifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASN1Identifier.swift; sourceTree = ""; }; + DCE6D57E2CF5F63100865D6E /* ASN1Object.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASN1Object.swift; sourceTree = ""; }; + DCE6D57F2CF5F63100865D6E /* OID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OID.swift; sourceTree = ""; }; + DCE6D5802CF5F63100865D6E /* PKCS7.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PKCS7.swift; sourceTree = ""; }; + DCE6D5812CF5F63100865D6E /* X509Certificate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = X509Certificate.swift; sourceTree = ""; }; + DCE6D5822CF5F63100865D6E /* X509Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = X509Extension.swift; sourceTree = ""; }; + DCE6D5832CF5F63100865D6E /* X509ExtensionAltName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = X509ExtensionAltName.swift; sourceTree = ""; }; + DCE6D5842CF5F63100865D6E /* X509ExtensionClasses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = X509ExtensionClasses.swift; sourceTree = ""; }; + DCE6D5852CF5F63100865D6E /* X509PublicKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = X509PublicKey.swift; sourceTree = ""; }; + DCE6D5932CF5F65200865D6E /* BinaryReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryReader.swift; sourceTree = ""; }; EA294F3C29F6A07A00A0494E /* WMTOperationUIData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperationUIData.swift; sourceTree = ""; }; EA44366929F9294600DDEC1C /* WMTPostApprovaScreenReview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPostApprovaScreenReview.swift; sourceTree = ""; }; EA44366B29F9297100DDEC1C /* WMTPostApprovaScreenRedirect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPostApprovaScreenRedirect.swift; sourceTree = ""; }; @@ -370,6 +402,8 @@ DC9511F826EA02C100FF40AD /* WPNIntegration.swift */, DCAC559B2CE773E90070644A /* WMTProvisioningUtils.swift */, DCAC55BB2CEC954C0070644A /* WMTUtils.swift */, + DCE6D5732CF5F46000865D6E /* WMTSignatureAPNSEnvironmentDetector.swift */, + DCE6D5752CF5F5C900865D6E /* MachO */, ); path = Common; sourceTree = ""; @@ -449,6 +483,36 @@ path = Requests; sourceTree = ""; }; + DCE6D5752CF5F5C900865D6E /* MachO */ = { + isa = PBXGroup; + children = ( + DCE6D5862CF5F63100865D6E /* CertParser */, + DCE6D5932CF5F65200865D6E /* BinaryReader.swift */, + DCE6D5782CF5F5E400865D6E /* Entitlements.swift */, + DCE6D5762CF5F5D500865D6E /* MachOReader.swift */, + ); + path = MachO; + sourceTree = ""; + }; + DCE6D5862CF5F63100865D6E /* CertParser */ = { + isa = PBXGroup; + children = ( + DCE6D57A2CF5F63100865D6E /* ASN1Decoder.swift */, + DCE6D57B2CF5F63100865D6E /* ASN1DistinguishedNames.swift */, + DCE6D57C2CF5F63100865D6E /* ASN1Encoder.swift */, + DCE6D57D2CF5F63100865D6E /* ASN1Identifier.swift */, + DCE6D57E2CF5F63100865D6E /* ASN1Object.swift */, + DCE6D57F2CF5F63100865D6E /* OID.swift */, + DCE6D5802CF5F63100865D6E /* PKCS7.swift */, + DCE6D5812CF5F63100865D6E /* X509Certificate.swift */, + DCE6D5822CF5F63100865D6E /* X509Extension.swift */, + DCE6D5832CF5F63100865D6E /* X509ExtensionAltName.swift */, + DCE6D5842CF5F63100865D6E /* X509ExtensionClasses.swift */, + DCE6D5852CF5F63100865D6E /* X509PublicKey.swift */, + ); + path = CertParser; + sourceTree = ""; + }; EA6DDF0D29F8031F0011E234 /* Screens */ = { isa = PBXGroup; children = ( @@ -616,10 +680,12 @@ DC8CB202244DCBE2009DDAA3 /* WMTOperations.swift in Sources */, DC48803E292282FF00DB844B /* WMTInboxMessage.swift in Sources */, DCC5CCB52449F8E9004679AC /* WMTOperationAttributeAmount.swift in Sources */, + DCE6D5942CF5F65200865D6E /* BinaryReader.swift in Sources */, DCC5CCD6244DBB7F004679AC /* WMTPushRegistrationData.swift in Sources */, DC3D0B392480F886000DC4D9 /* WMTLocalOperation.swift in Sources */, DCD8B336246C1BAF00385F02 /* WMTRejectionReason.swift in Sources */, DCC5CCD8244DBBBD004679AC /* WMTAuthorizationData.swift in Sources */, + DCE6D5792CF5F5E400865D6E /* Entitlements.swift in Sources */, DCAC55BC2CEC954C0070644A /* WMTUtils.swift in Sources */, DC488040292282FF00DB844B /* WMTInboxCount.swift in Sources */, DCA43C6B29927C960059A163 /* WMTOperationAttributeAmountConversion.swift in Sources */, @@ -650,6 +716,7 @@ DCC5CCCE244DB0AD004679AC /* WMTLogger.swift in Sources */, DCC5CCAE2449F7AC004679AC /* WMTUserOperation.swift in Sources */, DC9511F926EA02C100FF40AD /* WPNIntegration.swift in Sources */, + DCE6D5772CF5F5D500865D6E /* MachOReader.swift in Sources */, DCC5CCBD2449F965004679AC /* WMTOperationAttributeHeading.swift in Sources */, DCAC559C2CE773E90070644A /* WMTProvisioningUtils.swift in Sources */, DC8CB206244DD007009DDAA3 /* WMTAllowedOperationSignature.swift in Sources */, @@ -657,6 +724,19 @@ BFEEB20529379C700047941D /* WMTInboxGetMessageDetail.swift in Sources */, EACAF7B02A126B7D0021CA54 /* WMTJsonValue.swift in Sources */, DCAB7BCA24580BAC0006989D /* WMTQROperation.swift in Sources */, + DCE6D5872CF5F63100865D6E /* X509PublicKey.swift in Sources */, + DCE6D5882CF5F63100865D6E /* X509Extension.swift in Sources */, + DCE6D5892CF5F63100865D6E /* ASN1DistinguishedNames.swift in Sources */, + DCE6D58A2CF5F63100865D6E /* X509ExtensionClasses.swift in Sources */, + DCE6D58B2CF5F63100865D6E /* ASN1Decoder.swift in Sources */, + DCE6D58C2CF5F63100865D6E /* ASN1Object.swift in Sources */, + DCE6D58D2CF5F63100865D6E /* PKCS7.swift in Sources */, + DCE6D58E2CF5F63100865D6E /* OID.swift in Sources */, + DCE6D58F2CF5F63100865D6E /* X509Certificate.swift in Sources */, + DCE6D5902CF5F63100865D6E /* ASN1Encoder.swift in Sources */, + DCE6D5912CF5F63100865D6E /* ASN1Identifier.swift in Sources */, + DCE6D5922CF5F63100865D6E /* X509ExtensionAltName.swift in Sources */, + DCE6D5742CF5F46000865D6E /* WMTSignatureAPNSEnvironmentDetector.swift in Sources */, DCC5CCBF2449F981004679AC /* WMTOperationAttributePartyInfo.swift in Sources */, DC81D1CB244F451E00F80CD6 /* WMTPushImpl.swift in Sources */, EA9CE2BE2AEAA9FD00FE4E35 /* WMTProximityCheck.swift in Sources */, diff --git a/WultraMobileTokenSDK/Common/MachO/BinaryReader.swift b/WultraMobileTokenSDK/Common/MachO/BinaryReader.swift new file mode 100644 index 0000000..ef76547 --- /dev/null +++ b/WultraMobileTokenSDK/Common/MachO/BinaryReader.swift @@ -0,0 +1,47 @@ +// +// Copyright 2024 Wultra s.r.o. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions +// and limitations under the License. +// + +import Foundation + +internal class BinaryReader { + + private let handle: FileHandle + + init?(_ path: String) { + guard let binaryHandle = FileHandle(forReadingAtPath: path) else { + return nil + } + handle = binaryHandle + } + + var currentOffset: UInt64 { handle.offsetInFile } + + func seek(to offset: UInt64) { + handle.seek(toFileOffset: offset) + } + + func read() -> T { + handle.readData(ofLength: MemoryLayout.size).withUnsafeBytes({ $0.load(as: T.self) }) + } + + func readData(ofLength length: Int) -> Data { + handle.readData(ofLength: length) + } + + deinit { + handle.closeFile() + } +} diff --git a/WultraMobileTokenSDK/Common/MachO/CertParser/ASN1Decoder.swift b/WultraMobileTokenSDK/Common/MachO/CertParser/ASN1Decoder.swift new file mode 100644 index 0000000..83a38ab --- /dev/null +++ b/WultraMobileTokenSDK/Common/MachO/CertParser/ASN1Decoder.swift @@ -0,0 +1,268 @@ +// +// ASN1DERDecoder.swift +// +// Copyright © 2017 Filippo Maguolo. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +internal class ASN1DERDecoder { + + static func decode(data: Data) throws -> [ASN1Object] { + var iterator = data.makeIterator() + return try parse(iterator: &iterator) + } + + private static func parse(iterator: inout Data.Iterator) throws -> [ASN1Object] { + + var result: [ASN1Object] = [] + + while let nextValue = iterator.next() { + + let asn1obj = ASN1Object() + asn1obj.identifier = ASN1Identifier(rawValue: nextValue) + + if asn1obj.identifier!.isConstructed() { + + let contentData = try loadSubContent(iterator: &iterator) + + if contentData.isEmpty { + asn1obj.sub = try parse(iterator: &iterator) + } else { + var subIterator = contentData.makeIterator() + asn1obj.sub = try parse(iterator: &subIterator) + } + + asn1obj.value = nil + + asn1obj.rawValue = Data(contentData) + + for item in asn1obj.sub! { + item.parent = asn1obj + } + } else { + + if asn1obj.identifier!.typeClass() == .universal { + + var contentData = try loadSubContent(iterator: &iterator) + + asn1obj.rawValue = Data(contentData) + + // decode the content data with come more convenient format + + switch asn1obj.identifier!.tagNumber() { + + case .endOfContent: + return result + + case .boolean: + if let value = contentData.first { + asn1obj.value = value > 0 ? true : false + + } + + case .integer: + while contentData.first == 0 { + contentData.remove(at: 0) // remove not significant digit + } + asn1obj.value = contentData + + case .null: + asn1obj.value = nil + + case .objectIdentifier: + asn1obj.value = decodeOid(contentData: &contentData) + + case .utf8String, + .printableString, + .numericString, + .generalString, + .universalString, + .characterString, + .t61String: + + asn1obj.value = String(data: contentData, encoding: .utf8) + + case .bmpString: + asn1obj.value = String(data: contentData, encoding: .unicode) + + case .visibleString, + .ia5String: + + asn1obj.value = String(data: contentData, encoding: .ascii) + + case .utcTime: + asn1obj.value = dateFormatter(contentData: &contentData, + formats: ["yyMMddHHmmssZ", "yyMMddHHmmZ"]) + + case .generalizedTime: + asn1obj.value = dateFormatter(contentData: &contentData, + formats: ["yyyyMMddHHmmssZ"]) + + case .bitString: + if contentData.count > 0 { + _ = contentData.remove(at: 0) // unused bits + } + asn1obj.value = contentData + + case .octetString: + do { + var subIterator = contentData.makeIterator() + asn1obj.sub = try parse(iterator: &subIterator) + } catch { + if let str = String(data: contentData, encoding: .utf8) { + asn1obj.value = str + } else { + asn1obj.value = contentData + } + } + + default: + asn1obj.value = contentData + } + } else { + // custom/private tag + + let contentData = try loadSubContent(iterator: &iterator) + asn1obj.rawValue = Data(contentData) + + if let str = String(data: contentData, encoding: .utf8) { + asn1obj.value = str + } else { + asn1obj.value = contentData + } + } + } + result.append(asn1obj) + } + return result + } + + // Decode DER OID bytes to String with dot notation + static func decodeOid(contentData: inout Data) -> String? { + if contentData.isEmpty { + return nil + } + + var oid: String = "" + + let first = Int(contentData.remove(at: 0)) + oid.append("\(first / 40).\(first % 40)") + + var t = 0 + while contentData.count > 0 { + let n = Int(contentData.remove(at: 0)) + t = (t << 7) | (n & 0x7F) + if (n & 0x80) == 0 { + oid.append(".\(t)") + t = 0 + } + } + return oid + } + + private static func dateFormatter(contentData: inout Data, formats: [String]) -> Date? { + guard let str = String(data: contentData, encoding: .utf8) else { return nil } + for format in formats { + let fmt = DateFormatter() + fmt.locale = Locale(identifier: "en_US_POSIX") + fmt.dateFormat = format + if let dt = fmt.date(from: str) { + return dt + } + } + return nil + } +} + +internal enum ASN1Error: Error { + case parseError + case outOfBuffer +} + +internal extension Data { + func toIntValue() -> UInt64? { + if self.count > 8 { // check if suitable for UInt64 + return nil + } + + var value: UInt64 = 0 + for (index, byte) in self.enumerated() { + value += UInt64(byte) << UInt64(8*(count-index-1)) + } + return value + } +} + +internal extension Data { + var sequenceContent: Data { + var iterator = self.makeIterator() + _ = iterator.next() + do { + return try loadSubContent(iterator: &iterator) + } catch { + return self + } + } +} + +// Decode the number of bytes of the content +private func getContentLength(iterator: inout Data.Iterator) -> UInt64 { + let first = iterator.next() + + guard first != nil else { + return 0 + } + + if (first! & 0x80) != 0 { // long + let octetsToRead = first! - 0x80 + var data = Data() + for _ in 0.. Data { + + let len = getContentLength(iterator: &iterator) + + guard len < Int.max else { + return Data() + } + + var byteArray: [UInt8] = [] + + for _ in 0.. String { + var result = "" + let oidNames: [ASN1DistinguishedNames] = [ + .commonName, + .dnQualifier, + .serialNumber, + .givenName, + .surname, + .organizationalUnitName, + .organizationName, + .streetAddress, + .localityName, + .stateOrProvinceName, + .countryName, + .email + ] + for oidName in oidNames { + guard let oidBlock = block.findOid(oidName.oid) else { + continue + } + if !result.isEmpty { + // RFC allow "," or ";" and an optional additional space before and after + result.append(", ") + } + result.append(oidName.representation) + result.append("=") + if let value = oidBlock.parent?.sub?.last?.value as? String { + result.append(quote(string: value)) + } + } + return result + } + + class func quote(string: String) -> String { + let specialChar = ",+=\n<>#;\\" + if string.contains(where: { specialChar.contains($0) }) { + return "\"" + string + "\"" + } else { + return string + } + } +} diff --git a/WultraMobileTokenSDK/Common/MachO/CertParser/ASN1Encoder.swift b/WultraMobileTokenSDK/Common/MachO/CertParser/ASN1Encoder.swift new file mode 100644 index 0000000..174d4b9 --- /dev/null +++ b/WultraMobileTokenSDK/Common/MachO/CertParser/ASN1Encoder.swift @@ -0,0 +1,58 @@ +// +// ASN1Encoder.swift +// ASN1Decoder +// +// Copyright © 2020 Filippo Maguolo. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +internal class ASN1DEREncoder { + + static func encodeSequence(content: Data) -> Data { + var encoded = Data() + encoded.append(ASN1Identifier.constructedTag | ASN1Identifier.TagNumber.sequence.rawValue) + encoded.append(contentLength(of: content.count)) + encoded.append(content) + return encoded + } + + private static func contentLength(of size: Int) -> Data { + if size >= 128 { + var lenBytes = byteArray(from: size) + while lenBytes.first == 0 { lenBytes.removeFirst() } + let len: UInt8 = 0x80 | UInt8(lenBytes.count) + return Data([len] + lenBytes) + } else { + return Data([UInt8(size)]) + } + } + + private static func byteArray(from value: T) -> [UInt8] where T: FixedWidthInteger { + return withUnsafeBytes(of: value.bigEndian, Array.init) + } + +} + +internal extension Data { + var derEncodedSequence: Data { + return ASN1DEREncoder.encodeSequence(content: self) + } +} diff --git a/WultraMobileTokenSDK/Common/MachO/CertParser/ASN1Identifier.swift b/WultraMobileTokenSDK/Common/MachO/CertParser/ASN1Identifier.swift new file mode 100644 index 0000000..269c466 --- /dev/null +++ b/WultraMobileTokenSDK/Common/MachO/CertParser/ASN1Identifier.swift @@ -0,0 +1,100 @@ +// +// ASN1Identifier.swift +// +// Copyright © 2017 Filippo Maguolo. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +internal class ASN1Identifier: CustomStringConvertible { + + enum Class: UInt8 { + case universal = 0x00 + case application = 0x40 + case contextSpecific = 0x80 + case `private` = 0xC0 + } + + enum TagNumber: UInt8 { + case endOfContent = 0x00 + case boolean = 0x01 + case integer = 0x02 + case bitString = 0x03 + case octetString = 0x04 + case null = 0x05 + case objectIdentifier = 0x06 + case objectDescriptor = 0x07 + case external = 0x08 + case read = 0x09 + case enumerated = 0x0A + case embeddedPdv = 0x0B + case utf8String = 0x0C + case relativeOid = 0x0D + case sequence = 0x10 + case set = 0x11 + case numericString = 0x12 + case printableString = 0x13 + case t61String = 0x14 + case videotexString = 0x15 + case ia5String = 0x16 + case utcTime = 0x17 + case generalizedTime = 0x18 + case graphicString = 0x19 + case visibleString = 0x1A + case generalString = 0x1B + case universalString = 0x1C + case characterString = 0x1D + case bmpString = 0x1E + } + + static let constructedTag: UInt8 = 0x20 + + var rawValue: UInt8 + + init(rawValue: UInt8) { + self.rawValue = rawValue + } + + func typeClass() -> Class { + for tc in [Class.application, Class.contextSpecific, Class.private] where (rawValue & tc.rawValue) == tc.rawValue { + return tc + } + return .universal + } + + func isPrimitive() -> Bool { + return (rawValue & ASN1Identifier.constructedTag) == 0 + } + func isConstructed() -> Bool { + return (rawValue & ASN1Identifier.constructedTag) != 0 + } + + func tagNumber() -> TagNumber { + return TagNumber(rawValue: rawValue & 0x1F) ?? .endOfContent + } + + var description: String { + if typeClass() == .universal { + return String(describing: tagNumber()) + } else { + return "\(typeClass())(\(tagNumber().rawValue))" + } + } +} diff --git a/WultraMobileTokenSDK/Common/MachO/CertParser/ASN1Object.swift b/WultraMobileTokenSDK/Common/MachO/CertParser/ASN1Object.swift new file mode 100644 index 0000000..3f0b735 --- /dev/null +++ b/WultraMobileTokenSDK/Common/MachO/CertParser/ASN1Object.swift @@ -0,0 +1,105 @@ +// +// ASN1Object.swift +// +// Copyright © 2017 Filippo Maguolo. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +internal class ASN1Object: CustomStringConvertible { + + /// This property contains the DER encoded object + var rawValue: Data? + + /// This property contains the decoded Swift object whenever is possible + var value: Any? + + var identifier: ASN1Identifier? + + var sub: [ASN1Object]? + + weak var parent: ASN1Object? + + func sub(_ index: Int) -> ASN1Object? { + if let sub = self.sub, index >= 0, index < sub.count { + return sub[index] + } + return nil + } + + func subCount() -> Int { + return sub?.count ?? 0 + } + + func findOid(_ oid: OID) -> ASN1Object? { + return findOid(oid.rawValue) + } + + func findOid(_ oid: String) -> ASN1Object? { + for child in sub ?? [] { + if child.identifier?.tagNumber() == .objectIdentifier { + if child.value as? String == oid { + return child + } + } else { + if let result = child.findOid(oid) { + return result + } + } + } + return nil + } + + var description: String { + return printAsn1() + } + + var asString: String? { + if let string = value as? String { + return string + } + + for item in sub ?? [] { + if let string = item.asString { + return string + } + } + + return nil + } + + fileprivate func printAsn1(insets: String = "") -> String { + var output = insets + output.append(identifier?.description.uppercased() ?? "") + output.append(value != nil ? ": \(value!)": "") + if identifier?.typeClass() == .universal, identifier?.tagNumber() == .objectIdentifier { + if let oidName = OID.description(of: value as? String ?? "") { + output.append(" (\(oidName))") + } + } + output.append(sub != nil && sub!.count > 0 ? " {": "") + output.append("\n") + for item in sub ?? [] { + output.append(item.printAsn1(insets: insets + " ")) + } + output.append(sub != nil && sub!.count > 0 ? insets + "}\n": "") + return output + } +} diff --git a/WultraMobileTokenSDK/Common/MachO/CertParser/OID.swift b/WultraMobileTokenSDK/Common/MachO/CertParser/OID.swift new file mode 100644 index 0000000..6592e94 --- /dev/null +++ b/WultraMobileTokenSDK/Common/MachO/CertParser/OID.swift @@ -0,0 +1,101 @@ +// +// Copyright © 2017 Filippo Maguolo. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +internal enum OID: String { + case etsiQcsCompliance = "0.4.0.1862.1.1" + case etsiQcsRetentionPeriod = "0.4.0.1862.1.3" + case etsiQcsQcSSCD = "0.4.0.1862.1.4" + case dsa = "1.2.840.10040.4.1" + case ecPublicKey = "1.2.840.10045.2.1" + case prime256v1 = "1.2.840.10045.3.1.7" + case ecdsaWithSHA256 = "1.2.840.10045.4.3.2" + case ecdsaWithSHA512 = "1.2.840.10045.4.3.4" + case rsaEncryption = "1.2.840.113549.1.1.1" + case sha256WithRSAEncryption = "1.2.840.113549.1.1.11" + case md5WithRSAEncryption = "1.2.840.113549.1.1.4" + case sha1WithRSAEncryption = "1.2.840.113549.1.1.5" + + // Digest algorithms + case sha1 = "1.3.14.3.2.26" + case pkcsSha256 = "1.3.6.1.4.1.22554.1.2.1" + case sha2Family = "1.3.6.1.4.1.22554.1.2" + case sha3_244 = "2.16.840.1.101.3.4.2.7" + case sha3_256 = "2.16.840.1.101.3.4.2.8" + case sha3_384 = "2.16.840.1.101.3.4.2.9" + case md5 = "0.2.262.1.10.1.3.2" + + case pkcs7data = "1.2.840.113549.1.7.1" + case pkcs7signedData = "1.2.840.113549.1.7.2" + case pkcs7envelopedData = "1.2.840.113549.1.7.3" + case emailAddress = "1.2.840.113549.1.9.1" + case signingCertificateV2 = "1.2.840.113549.1.9.16.2.47" + case contentType = "1.2.840.113549.1.9.3" + case messageDigest = "1.2.840.113549.1.9.4" + case signingTime = "1.2.840.113549.1.9.5" + case certificateExtension = "1.3.6.1.4.1.11129.2.4.2" + case jurisdictionOfIncorporationSP = "1.3.6.1.4.1.311.60.2.1.2" + case jurisdictionOfIncorporationC = "1.3.6.1.4.1.311.60.2.1.3" + case authorityInfoAccess = "1.3.6.1.5.5.7.1.1" + case qcStatements = "1.3.6.1.5.5.7.1.3" + case cps = "1.3.6.1.5.5.7.2.1" + case unotice = "1.3.6.1.5.5.7.2.2" + case serverAuth = "1.3.6.1.5.5.7.3.1" + case clientAuth = "1.3.6.1.5.5.7.3.2" + case ocsp = "1.3.6.1.5.5.7.48.1" + case caIssuers = "1.3.6.1.5.5.7.48.2" + case dateOfBirth = "1.3.6.1.5.5.7.9.1" + case sha256 = "2.16.840.1.101.3.4.2.1" + case VeriSignEVpolicy = "2.16.840.1.113733.1.7.23.6" + case extendedValidation = "2.23.140.1.1" + case organizationValidated = "2.23.140.1.2.2" + case subjectKeyIdentifier = "2.5.29.14" + case keyUsage = "2.5.29.15" + case subjectAltName = "2.5.29.17" + case issuerAltName = "2.5.29.18" + case basicConstraints = "2.5.29.19" + case cRLDistributionPoints = "2.5.29.31" + case certificatePolicies = "2.5.29.32" + case authorityKeyIdentifier = "2.5.29.35" + case extKeyUsage = "2.5.29.37" + case subjectDirectoryAttributes = "2.5.29.9" + case organizationName = "2.5.4.10" + case organizationalUnitName = "2.5.4.11" + case businessCategory = "2.5.4.15" + case postalCode = "2.5.4.17" + case commonName = "2.5.4.3" + case surname = "2.5.4.4" + case givenName = "2.5.4.42" + case dnQualifier = "2.5.4.46" + case serialNumber = "2.5.4.5" + case countryName = "2.5.4.6" + case localityName = "2.5.4.7" + case stateOrProvinceName = "2.5.4.8" + case streetAddress = "2.5.4.9" + + static func description(of value: String) -> String? { + guard let oid = OID(rawValue: value) else { + return nil + } + return "\(oid)" + } +} diff --git a/WultraMobileTokenSDK/Common/MachO/CertParser/PKCS7.swift b/WultraMobileTokenSDK/Common/MachO/CertParser/PKCS7.swift new file mode 100644 index 0000000..e46fbbe --- /dev/null +++ b/WultraMobileTokenSDK/Common/MachO/CertParser/PKCS7.swift @@ -0,0 +1,99 @@ +// +// PKCS7.swift +// +// Copyright © 2017 Filippo Maguolo. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +internal class PKCS7 { + let derData: Data + let asn1: [ASN1Object] + let mainBlock: ASN1Object + + init(data: Data) throws { + derData = data + asn1 = try ASN1DERDecoder.decode(data: derData) + + guard let firstBlock = asn1.first, + let mainBlock = firstBlock.sub(1)?.sub(0) else { + throw PKCS7Error.parseError + } + + self.mainBlock = mainBlock + + guard firstBlock.sub(0)?.value as? String == OID.pkcs7signedData.rawValue else { + throw PKCS7Error.notSupported + } + } + + var digestAlgorithm: String? { + if let block = mainBlock.sub(1) { + return firstLeafValue(block: block) as? String + } + return nil + } + + var digestAlgorithmName: String? { + return OID.description(of: digestAlgorithm ?? "") ?? digestAlgorithm + } + + var certificate: X509Certificate? { + return mainBlock.sub(3)?.sub?.first.map { try? X509Certificate(asn1: $0) } ?? nil + } + + var certificates: [X509Certificate] { + return mainBlock.sub(3)?.sub?.compactMap { try? X509Certificate(asn1: $0) } ?? [] + } + + var data: Data? { + if let block = mainBlock.findOid(.pkcs7data) { + if let dataBlock = block.parent?.sub?.last { + var out = Data() + if let value = dataBlock.value as? Data { + out.append(value) + } else if dataBlock.value is String, let rawValue = dataBlock.rawValue { + out.append(rawValue) + } else { + for sub in dataBlock.sub ?? [] { + if let value = sub.value as? Data { + out.append(value) + } else if sub.value is String, let rawValue = sub.rawValue { + out.append(rawValue) + } else { + for sub2 in sub.sub ?? [] { + if let value = sub2.rawValue { + out.append(value) + } + } + } + } + } + return out.count > 0 ? out : nil + } + } + return nil + } +} + +internal enum PKCS7Error: Error { + case notSupported + case parseError +} diff --git a/WultraMobileTokenSDK/Common/MachO/CertParser/X509Certificate.swift b/WultraMobileTokenSDK/Common/MachO/CertParser/X509Certificate.swift new file mode 100644 index 0000000..e6fbd3b --- /dev/null +++ b/WultraMobileTokenSDK/Common/MachO/CertParser/X509Certificate.swift @@ -0,0 +1,344 @@ +// +// X509Certificate.swift +// +// Copyright © 2017 Filippo Maguolo. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +internal class X509Certificate: CustomStringConvertible { + + private let asn1: [ASN1Object] + private let block1: ASN1Object + let derData: Data? + + private static let beginPemBlock = "-----BEGIN CERTIFICATE-----" + private static let endPemBlock = "-----END CERTIFICATE-----" + + enum X509BlockPosition: Int { + case version = 0 + case serialNumber = 1 + case signatureAlg = 2 + case issuer = 3 + case dateValidity = 4 + case subject = 5 + case publicKey = 6 + case extensions = 7 + } + + convenience init(data: Data) throws { + if String(data: data, encoding: .utf8)?.contains(X509Certificate.beginPemBlock) ?? false { + try self.init(pem: data) + } else { + try self.init(der: data) + } + } + + init(der: Data) throws { + asn1 = try ASN1DERDecoder.decode(data: der) + guard asn1.count > 0, + let block1 = asn1.first?.sub(0) else { + throw ASN1Error.parseError + } + + self.block1 = block1 + self.derData = der + } + + convenience init(pem: Data) throws { + guard let derData = X509Certificate.decodeToDER(pem: pem) else { + throw ASN1Error.parseError + } + + try self.init(der: derData) + } + + init(asn1: ASN1Object) throws { + guard let block1 = asn1.sub(0) else { throw ASN1Error.parseError } + + self.asn1 = [asn1] + self.block1 = block1 + if let rawValue = asn1.rawValue { + self.derData = ASN1DEREncoder.encodeSequence(content: rawValue) + } else { + self.derData = nil + } + + } + + var description: String { + return asn1.reduce("") { $0 + "\($1.description)\n" } + } + + /// Checks that the given date is within the certificate's validity period. + func checkValidity(_ date: Date = Date()) -> Bool { + if let notBefore = notBefore, let notAfter = notAfter { + return date > notBefore && date < notAfter + } + return false + } + + /// Gets the version (version number) value from the certificate. + var version: Int? { + if let v = firstLeafValue(block: block1) as? Data, let index = v.toIntValue() { + return Int(index) + 1 + } + return nil + } + + /// Gets the serialNumber value from the certificate. + var serialNumber: Data? { + return block1[X509BlockPosition.serialNumber]?.value as? Data + } + + /// Returns the issuer (issuer distinguished name) value from the certificate as a String. + var issuerDistinguishedName: String? { + if let issuerBlock = block1[X509BlockPosition.issuer] { + return ASN1DistinguishedNames.string(from: issuerBlock) + } + return nil + } + + var issuerOIDs: [String] { + var result: [String] = [] + if let subjectBlock = block1[X509BlockPosition.issuer] { + for sub in subjectBlock.sub ?? [] { + if let value = firstLeafValue(block: sub) as? String { + result.append(value) + } + } + } + return result + } + + func issuer(oid: String) -> String? { + if let subjectBlock = block1[X509BlockPosition.issuer] { + if let oidBlock = subjectBlock.findOid(oid) { + return oidBlock.parent?.sub?.last?.value as? String + } + } + return nil + } + + func issuer(dn: ASN1DistinguishedNames) -> String? { + return issuer(oid: dn.oid) + } + + /// Returns the subject (subject distinguished name) value from the certificate as a String. + var subjectDistinguishedName: String? { + if let subjectBlock = block1[X509BlockPosition.subject] { + return ASN1DistinguishedNames.string(from: subjectBlock) + } + return nil + } + + var subjectOIDs: [String] { + var result: [String] = [] + if let subjectBlock = block1[X509BlockPosition.subject] { + for sub in subjectBlock.sub ?? [] { + if let value = firstLeafValue(block: sub) as? String { + result.append(value) + } + } + } + return result + } + + func subject(oid: String) -> String? { + if let subjectBlock = block1[X509BlockPosition.subject] { + if let oidBlock = subjectBlock.findOid(oid) { + return oidBlock.parent?.sub?.last?.value as? String + } + } + return nil + } + + func subject(dn: ASN1DistinguishedNames) -> String? { + return subject(oid: dn.oid) + } + + /// Gets the notBefore date from the validity period of the certificate. + var notBefore: Date? { + return block1[X509BlockPosition.dateValidity]?.sub(0)?.value as? Date + } + + /// Gets the notAfter date from the validity period of the certificate. + var notAfter: Date? { + return block1[X509BlockPosition.dateValidity]?.sub(1)?.value as? Date + } + + /// Gets the signature value (the raw signature bits) from the certificate. + var signature: Data? { + return asn1[0].sub(2)?.value as? Data + } + + /// Gets the signature algorithm name for the certificate signature algorithm. + var sigAlgName: String? { + return OID.description(of: sigAlgOID ?? "") + } + + /// Gets the signature algorithm OID string from the certificate. + var sigAlgOID: String? { + return block1.sub(2)?.sub(0)?.value as? String + } + + /// Gets the DER-encoded signature algorithm parameters from this certificate's signature algorithm. + var sigAlgParams: Data? { + return nil + } + + /** + Gets a boolean array representing bits of the KeyUsage extension, (OID = 2.5.29.15). + ``` + KeyUsage ::= BIT STRING { + digitalSignature (0), + nonRepudiation (1), + keyEncipherment (2), + dataEncipherment (3), + keyAgreement (4), + keyCertSign (5), + cRLSign (6), + encipherOnly (7), + decipherOnly (8) + } + ``` + */ + var keyUsage: [Bool] { + var result: [Bool] = [] + if let oidBlock = block1.findOid(OID.keyUsage) { + let data = oidBlock.parent?.sub?.last?.sub(0)?.value as? Data + let bits: UInt8 = data?.first ?? 0 + for index in 0...7 { + let value = bits & UInt8(1 << index) != 0 + result.insert(value, at: 0) + } + } + return result + } + + /// Gets a list of Strings representing the OBJECT IDENTIFIERs of the ExtKeyUsageSyntax field of + /// the extended key usage extension, (OID = 2.5.29.37). + var extendedKeyUsage: [String] { + return extensionObject(oid: OID.extKeyUsage)?.valueAsStrings ?? [] + } + + /// Gets a collection of subject alternative names from the SubjectAltName extension, (OID = 2.5.29.17). + var subjectAlternativeNames: [String] { + return extensionObject(oid: OID.subjectAltName)?.alternativeNameAsStrings ?? [] + } + + /// Gets a collection of issuer alternative names from the IssuerAltName extension, (OID = 2.5.29.18). + var issuerAlternativeNames: [String] { + return extensionObject(oid: OID.issuerAltName)?.alternativeNameAsStrings ?? [] + } + + /// Gets the informations of the key from this certificate. + var publicKey: X509PublicKey? { + return block1[X509BlockPosition.publicKey].map(X509PublicKey.init) + } + + /// Get a list of critical extension OID codes + var criticalExtensionOIDs: [String] { + guard let extensionBlocks = extensionBlocks else { return [] } + return extensionBlocks + .map { X509Extension(block: $0) } + .filter { $0.isCritical } + .compactMap { $0.oid } + } + + /// Get a list of non critical extension OID codes + var nonCriticalExtensionOIDs: [String] { + guard let extensionBlocks = extensionBlocks else { return [] } + return extensionBlocks + .map { X509Extension(block: $0) } + .filter { !$0.isCritical } + .compactMap { $0.oid } + } + + private var extensionBlocks: [ASN1Object]? { + return block1[X509BlockPosition.extensions]?.sub(0)?.sub + } + + /// Gets the extension information of the given OID enum. + func extensionObject(oid: OID) -> X509Extension? { + return extensionObject(oid: oid.rawValue) + } + + /// Gets the extension information of the given OID code. + func extensionObject(oid: String) -> X509Extension? { + return block1[X509BlockPosition.extensions]? + .findOid(oid)? + .parent + .map { oidExtensionMap[oid]?.init(block: $0) ?? X509Extension(block: $0) } + } + + // Association of Class decoding helper and OID + private let oidExtensionMap: [String: X509Extension.Type] = [ + OID.basicConstraints.rawValue: BasicConstraintExtension.self, + OID.subjectKeyIdentifier.rawValue: SubjectKeyIdentifierExtension.self, + OID.authorityInfoAccess.rawValue: AuthorityInfoAccessExtension.self, + OID.authorityKeyIdentifier.rawValue: AuthorityKeyIdentifierExtension.self, + OID.certificatePolicies.rawValue: CertificatePoliciesExtension.self, + OID.cRLDistributionPoints.rawValue: CRLDistributionPointsExtension.self + ] + + // read possibile PEM encoding + private static func decodeToDER(pem pemData: Data) -> Data? { + if + let pem = String(data: pemData, encoding: .ascii), + pem.contains(beginPemBlock) { + + let lines = pem.components(separatedBy: .newlines) + var base64buffer = "" + var certLine = false + for line in lines { + if line == endPemBlock { + certLine = false + } + if certLine { + base64buffer.append(line) + } + if line == beginPemBlock { + certLine = true + } + } + if let derDataDecoded = Data(base64Encoded: base64buffer) { + return derDataDecoded + } + } + + return nil + } +} + +internal func firstLeafValue(block: ASN1Object) -> Any? { + if let sub = block.sub?.first { + return firstLeafValue(block: sub) + } + return block.value +} + +internal extension ASN1Object { + subscript(index: X509Certificate.X509BlockPosition) -> ASN1Object? { + guard let sub = sub, + sub.indices.contains(index.rawValue) else { return nil } + return sub[index.rawValue] + } +} diff --git a/WultraMobileTokenSDK/Common/MachO/CertParser/X509Extension.swift b/WultraMobileTokenSDK/Common/MachO/CertParser/X509Extension.swift new file mode 100644 index 0000000..7ce6d7a --- /dev/null +++ b/WultraMobileTokenSDK/Common/MachO/CertParser/X509Extension.swift @@ -0,0 +1,69 @@ +// +// X509Extension.swift +// +// Copyright © 2019 Filippo Maguolo. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +internal class X509Extension { + + let block: ASN1Object + + required init(block: ASN1Object) { + self.block = block + } + + var oid: String? { + return block.sub(0)?.value as? String + } + + var name: String? { + return OID.description(of: oid ?? "") + } + + var isCritical: Bool { + if block.sub?.count ?? 0 > 2 { + return block.sub(1)?.value as? Bool ?? false + } + return false + } + + var value: Any? { + if let valueBlock = block.sub?.last { + return firstLeafValue(block: valueBlock) + } + return nil + } + + var valueAsBlock: ASN1Object? { + return block.sub?.last + } + + var valueAsStrings: [String] { + var result: [String] = [] + for item in block.sub?.last?.sub?.last?.sub ?? [] { + if let name = item.value as? String { + result.append(name) + } + } + return result + } +} diff --git a/WultraMobileTokenSDK/Common/MachO/CertParser/X509ExtensionAltName.swift b/WultraMobileTokenSDK/Common/MachO/CertParser/X509ExtensionAltName.swift new file mode 100644 index 0000000..442f708 --- /dev/null +++ b/WultraMobileTokenSDK/Common/MachO/CertParser/X509ExtensionAltName.swift @@ -0,0 +1,82 @@ +// +// X509ExtensionAltName.swift +// +// Copyright © 2020 Filippo Maguolo. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +internal extension X509Extension { + + // Used for SubjectAltName and IssuerAltName + // Every name can be one of these subtype: + // - otherName [0] INSTANCE OF OTHER-NAME, + // - rfc822Name [1] IA5String, + // - dNSName [2] IA5String, + // - x400Address [3] ORAddress, + // - directoryName [4] Name, + // - ediPartyName [5] EDIPartyName, + // - uniformResourceIdentifier [6] IA5String, + // - IPAddress [7] OCTET STRING, + // - registeredID [8] OBJECT IDENTIFIER + // + // Result does not support: x400Address and ediPartyName + // + var alternativeNameAsStrings: [String] { + var result: [String] = [] + for item in block.sub?.last?.sub?.last?.sub ?? [] { + guard let name = generalName(of: item) else { + continue + } + result.append(name) + } + return result + } + + func generalName(of item: ASN1Object) -> String? { + guard let nameType = item.identifier?.tagNumber().rawValue else { + return nil + } + switch nameType { + case 0: + if let name = item.sub?.last?.sub?.last?.value as? String { + return name + } + case 1, 2, 6: + if let name = item.value as? String { + return name + } + case 4: + return ASN1DistinguishedNames.string(from: item) + case 7: + if let ip = item.value as? Data { + return ip.map({ "\($0)" }).joined(separator: ".") + } + case 8: + if let value = item.value as? String, var data = value.data(using: .utf8) { + let oid = ASN1DERDecoder.decodeOid(contentData: &data) + return oid + } + default: + return nil + } + return nil + } +} diff --git a/WultraMobileTokenSDK/Common/MachO/CertParser/X509ExtensionClasses.swift b/WultraMobileTokenSDK/Common/MachO/CertParser/X509ExtensionClasses.swift new file mode 100644 index 0000000..f2482c1 --- /dev/null +++ b/WultraMobileTokenSDK/Common/MachO/CertParser/X509ExtensionClasses.swift @@ -0,0 +1,181 @@ +// +// X509ExtensionClasses.swift +// +// Copyright © 2020 Filippo Maguolo. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +internal extension X509Certificate { + + /// Recognition for Basic Constraint Extension (2.5.29.19) + class BasicConstraintExtension: X509Extension { + + var isCA: Bool { + return (valueAsBlock?.sub(0)?.sub(0)?.value as? Bool) ?? false + } + + var pathLenConstraint: UInt64? { + guard let data = valueAsBlock?.sub(0)?.sub(1)?.value as? Data else { + return nil + } + return data.toIntValue() + } + } + + /// Recognition for Subject Key Identifier Extension (2.5.29.14) + class SubjectKeyIdentifierExtension: X509Extension { + + override var value: Any? { + guard let rawValue = valueAsBlock?.rawValue else { + return nil + } + return rawValue.sequenceContent + } + } + + // MARK: - Authority Extensions + + struct AuthorityInfoAccess { + let method: String + let location: String + } + + /// Recognition for Authority Info Access Extension (1.3.6.1.5.5.7.1.1) + class AuthorityInfoAccessExtension: X509Extension { + + var infoAccess: [AuthorityInfoAccess]? { + guard let valueAsBlock = valueAsBlock else { + return nil + } + let subs = valueAsBlock.sub(0)?.sub ?? [] + + return subs.compactMap { sub in + guard var oidData = sub.sub(0)?.rawValue, + let nameBlock = sub.sub(1) else { + return nil + } + if + let oid = ASN1DERDecoder.decodeOid(contentData: &oidData), + let location = generalName(of: nameBlock) { + return AuthorityInfoAccess(method: oid, location: location) + } else { + return nil + } + } + } + } + + /// Recognition for Authority Key Identifier Extension (2.5.29.35) + class AuthorityKeyIdentifierExtension: X509Extension { + + /* + AuthorityKeyIdentifier ::= SEQUENCE { + keyIdentifier [0] KeyIdentifier OPTIONAL, + authorityCertIssuer [1] GeneralNames OPTIONAL, + authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL } + */ + + var keyIdentifier: Data? { + guard let sequence = valueAsBlock?.sub(0)?.sub else { + return nil + } + if let sub = sequence.first(where: { $0.identifier?.tagNumber().rawValue == 0 }) { + return sub.rawValue + } + return nil + } + + var certificateIssuer: [String]? { + guard let sequence = valueAsBlock?.sub(0)?.sub else { + return nil + } + if let sub = sequence.first(where: { $0.identifier?.tagNumber().rawValue == 1 }) { + return sub.sub?.compactMap { generalName(of: $0) } + } + return nil + } + + var serialNumber: Data? { + guard let sequence = valueAsBlock?.sub(0)?.sub else { + return nil + } + if let sub = sequence.first(where: { $0.identifier?.tagNumber().rawValue == 2 }) { + return sub.rawValue + } + return nil + } + } + + // MARK: - Certificate Policies Extension + + struct CertificatePolicyQualifier { + let oid: String + let value: String? + } + struct CertificatePolicy { + let oid: String + let qualifiers: [CertificatePolicyQualifier]? + } + + /// Recognition for Certificate Policies Extension (2.5.29.32) + class CertificatePoliciesExtension: X509Extension { + + var policies: [CertificatePolicy]? { + guard let valueAsBlock = valueAsBlock else { + return nil + } + let subs = valueAsBlock.sub(0)?.sub ?? [] + + return subs.compactMap { sub in + guard + var data = sub.sub(0)?.rawValue, + let oid = ASN1DERDecoder.decodeOid(contentData: &data) else { + return nil + } + var qualifiers: [CertificatePolicyQualifier]? + if let subQualifiers = sub.sub(1) { + qualifiers = subQualifiers.sub?.compactMap { sub in + if var rawValue = sub.sub(0)?.rawValue, let oid = ASN1DERDecoder.decodeOid(contentData: &rawValue) { + let value = sub.sub(1)?.asString + return CertificatePolicyQualifier(oid: oid, value: value) + } else { + return nil + } + } + } + return CertificatePolicy(oid: oid, qualifiers: qualifiers) + } + } + } + + // MARK: - CRL Distribution Points + + class CRLDistributionPointsExtension: X509Extension { + + var crls: [String]? { + guard let valueAsBlock = valueAsBlock else { + return nil + } + let subs = valueAsBlock.sub(0)?.sub ?? [] + return subs.compactMap { $0.asString } + } + } +} diff --git a/WultraMobileTokenSDK/Common/MachO/CertParser/X509PublicKey.swift b/WultraMobileTokenSDK/Common/MachO/CertParser/X509PublicKey.swift new file mode 100644 index 0000000..48a1fa2 --- /dev/null +++ b/WultraMobileTokenSDK/Common/MachO/CertParser/X509PublicKey.swift @@ -0,0 +1,75 @@ +// +// X509PublicKey.swift +// +// Copyright © 2019 Filippo Maguolo. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +internal class X509PublicKey { + + let pkBlock: ASN1Object + + init(pkBlock: ASN1Object) { + self.pkBlock = pkBlock + } + + var algOid: String? { + return pkBlock.sub(0)?.sub(0)?.value as? String + } + + var algName: String? { + return OID.description(of: algOid ?? "") + } + + var algParams: String? { + return pkBlock.sub(0)?.sub(1)?.value as? String + } + + var derEncodedKey: Data? { + return pkBlock.rawValue?.derEncodedSequence + } + + var key: Data? { + guard + let algOid = algOid, + let oid = OID(rawValue: algOid), + let keyData = pkBlock.sub(1)?.value as? Data else { + return nil + } + + switch oid { + case .ecPublicKey: + return keyData + + case .rsaEncryption: + guard let publicKeyAsn1Objects = (try? ASN1DERDecoder.decode(data: keyData)) else { + return nil + } + guard let publicKeyModulus = publicKeyAsn1Objects.first?.sub(0)?.value as? Data else { + return nil + } + return publicKeyModulus + + default: + return nil + } + } +} diff --git a/WultraMobileTokenSDK/Common/MachO/Entitlements.swift b/WultraMobileTokenSDK/Common/MachO/Entitlements.swift new file mode 100644 index 0000000..5c63e2d --- /dev/null +++ b/WultraMobileTokenSDK/Common/MachO/Entitlements.swift @@ -0,0 +1,68 @@ +// +// Copyright 2024 Wultra s.r.o. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions +// and limitations under the License. +// + +import Foundation + +internal class Entitlements { + + enum Key: String { + case autofillCredentialProvider = "com.apple.developer.authentication-services.autofill-credential-provider" + case signWithApple = "com.apple.developer.applesignin" + case contacts = "com.apple.developer.contacts.notes" + case classKit = "com.apple.developer.ClassKit-environment" + case automaticAssesmentConfiguration = "com.apple.developer.automatic-assessment-configuration" + case gameCenter = "com.apple.developer.game-center" + case healthKit = "com.apple.developer.healthkit" + case healthKitCapabilities = "com.apple.developer.healthkit.access" + case homeKit = "com.apple.developer.homekit" + case iCloudDevelopmentContainersIdentifiers = "com.apple.developer.icloud-container-development-container-identifiers" + case iCloudContainersEnvironment = "com.apple.developer.icloud-container-environment" + case iCloudContainerIdentifiers = "com.apple.developer.icloud-container-identifiers" + case iCloudServices = "com.apple.developer.icloud-services" + case iCloudKeyValueStore = "com.apple.developer.ubiquity-kvstore-identifier" + case interAppAudio = "inter-app-audio" + case networkExtensions = "com.apple.developer.networking.networkextension" + case personalVPN = "com.apple.developer.networking.vpn.api" + case apsEnvironment = "aps-environment" + case appGroups = "com.apple.security.application-groups" + case keychainAccessGroups = "keychain-access-groups" + case dataProtection = "com.apple.developer.default-data-protection" + case siri = "com.apple.developer.siri" + case passTypeIDs = "com.apple.developer.pass-type-identifiers" + case merchantIDs = "com.apple.developer.in-app-payments" + case wifiInfo = "com.apple.developer.networking.wifi-info" + case externalAccessoryConfiguration = "com.apple.external-accessory.wireless-configuration" + case multipath = "com.apple.developer.networking.multipath" + case hotspotConfiguration = "com.apple.developer.networking.HotspotConfiguration" + case nfcTagReaderSessionFormats = "com.apple.developer.nfc.readersession.formats" + case associatedDomains = "com.apple.developer.associated-domains" + case maps = "com.apple.developer.maps" + case driverKit = "com.apple.developer.driverkit.transport.pci" + } + + private let values: [String: Any] + + init?(_ data: Data) { + guard let rawValues = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any] else { + return nil + } + self.values = rawValues + } + + func value(forKey key: Entitlements.Key) -> Any? { + values[key.rawValue] + } +} diff --git a/WultraMobileTokenSDK/Common/MachO/MachOReader.swift b/WultraMobileTokenSDK/Common/MachO/MachOReader.swift new file mode 100644 index 0000000..d513c26 --- /dev/null +++ b/WultraMobileTokenSDK/Common/MachO/MachOReader.swift @@ -0,0 +1,167 @@ +// +// Copyright 2024 Wultra s.r.o. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions +// and limitations under the License. +// + +import Foundation +import MachO +import CommonCrypto + +internal struct MachOSignatureBlob { + let pkcs: PKCS7? + let entitlemens: Entitlements? +} + +internal class MachOReader { + + private struct CSSuperBlob { + var magic: UInt32 + var length: UInt32 + var count: UInt32 + } + + private struct CSBlob { + var type: UInt32 + var offset: UInt32 + } + + private struct CSMagic { + static let embeddedSignature: UInt32 = 0xfade0cc0 + static let embeddedEntitlements: UInt32 = 0xfade7171 + static let blobWrapper: UInt32 = 0xfade0b01 + static let codeDirectory: UInt32 = 0xfade0c02 + } + + private enum BinaryType { + struct HeaderData { + let headerSize: Int + let commandCount: Int + } + struct FatHeaderData { + let archCount: Int + } + case singleArch(headerInfo: HeaderData) + case fat(header: FatHeaderData) + } + + private var blobs: [MachOSignatureBlob]! + + static func readSignatures(_ binaryPath: String) -> [MachOSignatureBlob]? { + MachOReader(binaryPath)?.blobs + } + + private init?(_ binaryPath: String) { + guard let binary = BinaryReader(binaryPath) else { + return nil + } + + switch getBinaryType(binary: binary) { + case .singleArch(let headerInfo): + let headerSize = headerInfo.headerSize + let commandCount = headerInfo.commandCount + blobs = [readSignatureFromBinarySlice(binary: binary, headerOffset: headerSize, dataOffset: 0, cmdCount: commandCount)] + case .fat(let header): + blobs = readSignaturesFromFatBinary(binary: binary, architectureCount: header.archCount, startingAt: MemoryLayout.size) + default: + return nil + } + } + + private func getBinaryType(binary: BinaryReader, fromSliceStartingAt offset: UInt64 = 0) -> BinaryType? { + binary.seek(to: offset) + let header: mach_header = binary.read() + let commandCount = Int(header.ncmds) + switch header.magic { + case MH_MAGIC: + let data = BinaryType.HeaderData(headerSize: MemoryLayout.size, commandCount: commandCount) + return .singleArch(headerInfo: data) + case MH_MAGIC_64: + let data = BinaryType.HeaderData(headerSize: MemoryLayout.size, commandCount: commandCount) + return .singleArch(headerInfo: data) + default: + binary.seek(to: 0) + let fatHeader: fat_header = binary.read() + if CFSwapInt32(fatHeader.magic) == FAT_MAGIC { + let archCount = Int(CFSwapInt32(fatHeader.nfat_arch)) + return .fat(header: BinaryType.FatHeaderData(archCount: archCount)) + } else { + return nil + } + } + } + + private func readSignaturesFromFatBinary(binary: BinaryReader, architectureCount: Int, startingAt: Int) -> [MachOSignatureBlob] { + var blobs = [MachOSignatureBlob]() + for i in 0...size) + binary.seek(to: UInt64(offset)) + let fatArch: fat_arch = binary.read() + let fatArchOffset = CFSwapInt32(fatArch.offset) + let arch = getBinaryType(binary: binary, fromSliceStartingAt: UInt64(fatArchOffset)) + switch arch { + case .singleArch(let headerInfo): + let headerOffset = Int(fatArchOffset) + headerInfo.headerSize + blobs.append(readSignatureFromBinarySlice(binary: binary, headerOffset: headerOffset, dataOffset: fatArchOffset, cmdCount: headerInfo.commandCount)) + default: + blobs.append(MachOSignatureBlob(pkcs: nil, entitlemens: nil)) + } + } + return blobs + } + + private func readSignatureFromBinarySlice(binary: BinaryReader, headerOffset: Int, dataOffset: UInt32, cmdCount: Int) -> MachOSignatureBlob { + binary.seek(to: UInt64(headerOffset)) + var blob: MachOSignatureBlob? + for _ in 0...size))) + } + return blob ?? MachOSignatureBlob(pkcs: nil, entitlemens: nil) + } + + private func readSignatureData(binary: BinaryReader, startingAt offset: UInt32) -> MachOSignatureBlob { + var pkcs: PKCS7? + var entitlements: Entitlements? + binary.seek(to: UInt64(offset)) + let metaBlob: CSSuperBlob = binary.read() + if CFSwapInt32(metaBlob.magic) == CSMagic.embeddedSignature { + let metaBlobSize = UInt32(MemoryLayout.size) + let blobSize = UInt32(MemoryLayout.size) + let itemCount = CFSwapInt32(metaBlob.count) + for index in 0.. WMTPushRegistrationEnvironment? { + + D.debug("Parsing main bundle signature to get APNS Environment.") + + guard let executableName = Bundle.main.infoDictionary?[kCFBundleExecutableKey as String] as? String else { + D.error("Could not read executable name from Info.plist") + return nil + } + guard let executablePath = Bundle.main.path(forResource: executableName, ofType: nil) else { + D.error("Could not find executable \(executableName)") + return nil + } + + guard let cmsList = MachOReader.readSignatures(executablePath) else { + D.error("Could not read signatures from \(executablePath)") + return nil + } + + guard cmsList.isEmpty == false else { + D.info("Executable is not signed.") + return nil + } + + for cms in cmsList { + + if let apnsEnvironment = cms.entitlemens?.value(forKey: .apsEnvironment) as? String { + switch apnsEnvironment { + case "development": + D.debug("Development entitlement found in the app signature") + return .development + case "production": + D.debug("Production entitlement found in the app signature") + return .production + default: + D.debug("Unknown entitlement found in the app signature: \(apnsEnvironment)") + } + } + + guard let pkcs = cms.pkcs else { + D.error("Could not read CMS signature") + return nil + } + + guard pkcs.certificates.count == 3 else { + D.error("Invalid CMS signature count") + return nil + } + + if pkcs.certificates.contains(where: { $0.subjectDistinguishedName == KnownCertificates.appleICAname }) && pkcs.certificates.contains(where: { $0.subjectDistinguishedName == KnownCertificates.appleOASname }) { + D.debug("App is signed with Apple's iOS Development Certificate - assuming production APNS environment") + return .production + } + + if pkcs.certificates.contains(where: { $0.subjectDistinguishedName == KnownCertificates.appleTFBname }) { + D.debug("App is signed with Apple's iOS TestFlight Beta Certificate - assuming production APNS environment") + return .production + } + } + + return nil + } +} + +public struct KnownCertificates { + static let appleICAname = "CN=Apple iPhone Certification Authority, OU=Certification Authority, O=Apple Inc., C=US" + static let appleOASname = "CN=Apple iPhone OS Application Signing, OU=iPhone, O=Apple Inc., C=US" + static let appleTFBname = "CN=TestFlight Beta Distribution, OU=TESTFLIGHT, O=Apple Inc., C=US" +} diff --git a/WultraMobileTokenSDK/Push/Service/WMTPushImpl.swift b/WultraMobileTokenSDK/Push/Service/WMTPushImpl.swift index 559081e..73d1d31 100644 --- a/WultraMobileTokenSDK/Push/Service/WMTPushImpl.swift +++ b/WultraMobileTokenSDK/Push/Service/WMTPushImpl.swift @@ -82,7 +82,7 @@ class WMTPushImpl: WMTPush, WMTService { case .apns(_, let environment): payloadPlatform = .apns payloadEnvironment = getPushEnvironment(environment: environment) - case .fcm(let token): + case .fcm(_): payloadPlatform = .fcm payloadEnvironment = nil // no env for FCM } @@ -131,7 +131,7 @@ class WMTPushImpl: WMTPush, WMTService { D.info("Using APNS production environment for push notifications.") return .production case .automatic: - let env = WMTProvisioningUtils.getMainProvisioningProfile()?.apnsEnvironment + let env = WMTProvisioningUtils.getMainProvisioningProfile()?.apnsEnvironment ?? WMTSignatureAPNSEnvironmentDetector.detectAPNSEnvironment() if let env { D.info("Using \(env) environment for push notifications (automatic resolution).") } else {