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