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