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