1 // Copyright 2015-present 650 Industries. All rights reserved. 2 3 import Foundation 4 5 internal typealias Certificate = (SecCertificate, X509Certificate) 6 7 internal final class ExpoProjectInformation: Equatable { 8 private(set) var projectId: String 9 private(set) var scopeKey: String 10 11 required init(projectId: String, scopeKey: String) { 12 self.projectId = projectId 13 self.scopeKey = scopeKey 14 } 15 ==null16 static func == (lhs: ExpoProjectInformation, rhs: ExpoProjectInformation) -> Bool { 17 return lhs.projectId == rhs.projectId && lhs.scopeKey == rhs.scopeKey 18 } 19 } 20 21 /** 22 * Full certificate chain for verifying code signing. 23 * The chain should look like the following: 24 * 0: code signing certificate 25 * 1...n-1: intermediate certificates 26 * n: root certificate 27 * 28 * Requirements: 29 * - Length(certificateChain) > 0 30 * - certificate chain is valid and each certificate is valid 31 * - 0th certificate is a valid code signing certificate 32 */ 33 internal final class CertificateChain { 34 // ASN.1 path to the extended key usage info within a CERT 35 static let CodeSigningCertificateExtendedUsageCodeSigningOID = "1.3.6.1.5.5.7.3.3" 36 // OID of expo project info, stored as `<projectId>,<scopeKey>` 37 static let CodeSigningCertificateExpoProjectInformationOID = "1.2.840.113556.1.8000.2554.43437.254.128.102.157.7894389.20439.2.1" 38 39 private var certificateStrings: [String] 40 41 required init(certificateStrings: [String]) throws { 42 self.certificateStrings = certificateStrings 43 } 44 codeSigningCertificatenull45 func codeSigningCertificate() throws -> Certificate { 46 if certificateStrings.isEmpty { 47 throw CodeSigningError.CertificateEmptyError 48 } 49 50 let certificateChain = try certificateStrings.map { certificateString throws in 51 try CertificateChain.constructCertificate(certificateString: certificateString) 52 } 53 try certificateChain.validateChain() 54 55 let leafCertificate = certificateChain.first! 56 let (_, x509LeafCertificate) = leafCertificate 57 if !x509LeafCertificate.isCodeSigningCertificate() { 58 throw CodeSigningError.CertificateMissingCodeSigningError 59 } 60 61 return leafCertificate 62 } 63 constructCertificatenull64 private static func constructCertificate(certificateString: String) throws -> Certificate { 65 guard let certificateData = certificateString.data(using: .utf8) else { 66 throw CodeSigningError.CertificateEncodingError 67 } 68 69 guard let certificateDataDer = Crypto.decodePEMToDER(pem: certificateData, pemType: .certificate) else { 70 throw CodeSigningError.CertificateDERDecodeError 71 } 72 73 let x509Certificate = try X509Certificate(der: certificateDataDer) 74 75 guard x509Certificate.checkValidity() else { 76 throw CodeSigningError.CertificateValidityError 77 } 78 79 guard let secCertificate = SecCertificateCreateWithData(nil, certificateDataDer as CFData) else { 80 throw CodeSigningError.CertificateDERDecodeError 81 } 82 83 return (secCertificate, x509Certificate) 84 } 85 } 86 87 internal extension X509Certificate { isCACertificatenull88 func isCACertificate() -> Bool { 89 if let ext = self.extensionObject(oid: .basicConstraints) as? X509Certificate.BasicConstraintExtension { 90 if !ext.isCA { 91 return false 92 } 93 } else { 94 return false 95 } 96 97 let keyUsage = self.keyUsage 98 if keyUsage.isEmpty || !keyUsage[5] { 99 return false 100 } 101 102 return true 103 } 104 isCodeSigningCertificatenull105 func isCodeSigningCertificate() -> Bool { 106 let keyUsage = self.keyUsage 107 if keyUsage.isEmpty || !keyUsage[0] { 108 return false 109 } 110 111 let extendedKeyUsage = self.extendedKeyUsage 112 if !extendedKeyUsage.contains(CertificateChain.CodeSigningCertificateExtendedUsageCodeSigningOID) { 113 return false 114 } 115 116 return true 117 } 118 expoProjectInformationnull119 func expoProjectInformation() throws -> ExpoProjectInformation? { 120 guard let projectInformationExtensionValue = extensionObject(oid: CertificateChain.CodeSigningCertificateExpoProjectInformationOID)?.value else { 121 return nil 122 } 123 124 guard let projectInformationExtensionValue = projectInformationExtensionValue as? String else { 125 throw CodeSigningError.InvalidExpoProjectInformationExtensionValue 126 } 127 128 let components = projectInformationExtensionValue 129 .components(separatedBy: ",") 130 .map { it in 131 it.trimmingCharacters(in: CharacterSet.whitespaces) 132 } 133 if components.count != 2 { 134 throw CodeSigningError.InvalidExpoProjectInformationExtensionValue 135 } 136 return ExpoProjectInformation(projectId: components[0], scopeKey: components[1]) 137 } 138 } 139 140 private extension Array where Element == Certificate { validateChainnull141 func validateChain() throws { 142 let (anchorSecCert, anchorX509Cert) = self.last! 143 144 // only trust anchor if self-signed 145 if anchorX509Cert.subjectDistinguishedName != anchorX509Cert.issuerDistinguishedName { 146 throw CodeSigningError.CertificateRootNotSelfSigned 147 } 148 149 let secCertificates = self.map { secCertificate, _ in 150 secCertificate 151 } 152 let trust = try SecTrust.create(certificates: secCertificates, policy: SecPolicyCreateBasicX509()) 153 try trust.setAnchorCertificates([anchorSecCert]) 154 try trust.disableNetwork() 155 try trust.evaluate() 156 157 if count > 1 { 158 let (_, rootX509Cert) = self.last! 159 if !rootX509Cert.isCACertificate() { 160 throw CodeSigningError.CertificateRootNotCA 161 } 162 163 var lastExpoProjectInformation = try rootX509Cert.expoProjectInformation() 164 // all certificates between (root, leaf] 165 for i in (0...(count - 2)).reversed() { 166 let (_, x509Cert) = self[i] 167 let currProjectInformation = try x509Cert.expoProjectInformation() 168 if lastExpoProjectInformation != nil && lastExpoProjectInformation != currProjectInformation { 169 throw CodeSigningError.CertificateProjectInformationChainError 170 } 171 lastExpoProjectInformation = currProjectInformation 172 } 173 } 174 } 175 } 176 177 private extension SecTrust { createnull178 static func create(certificates: [SecCertificate], policy: SecPolicy) throws -> SecTrust { 179 var optionalTrust: SecTrust? 180 let status = SecTrustCreateWithCertificates(certificates as AnyObject, policy, &optionalTrust) 181 guard let trust = optionalTrust, status.isSuccess else { 182 NSLog("Could not create sec trust with certificates (OSStatus: %@)", status) 183 throw CodeSigningError.CertificateChainError 184 } 185 return trust 186 } 187 setAnchorCertificatesnull188 func setAnchorCertificates(_ anchorCertificates: [SecCertificate]) throws { 189 let status = SecTrustSetAnchorCertificates(self, anchorCertificates as CFArray) 190 guard status.isSuccess else { 191 NSLog("Could not set anchor certificates on sec trust (OSStatus: %@)", status) 192 throw CodeSigningError.CertificateChainError 193 } 194 195 let status2 = SecTrustSetAnchorCertificatesOnly(self, true) 196 guard status2.isSuccess else { 197 NSLog("Could not set anchor certificates only setting on sec trust (OSStatus: %@)", status) 198 throw CodeSigningError.CertificateChainError 199 } 200 } 201 disableNetworknull202 func disableNetwork() throws { 203 let status = SecTrustSetNetworkFetchAllowed(self, false) 204 guard status.isSuccess else { 205 NSLog("Could not disable network fetch on sec trust (OSStatus: %@)", status) 206 throw CodeSigningError.CertificateChainError 207 } 208 } 209 evaluatenull210 func evaluate() throws { 211 var error: CFError? 212 let success = SecTrustEvaluateWithError(self, &error) 213 if !success { 214 if let error = error { 215 NSLog("Sec trust evaluation error: %@", error.localizedDescription) 216 } 217 throw CodeSigningError.CertificateChainError 218 } 219 } 220 } 221