8
8
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9
9
*/
10
10
11
+ import Dispatch
12
+ import struct Foundation. Data
13
+ import struct Foundation. Date
14
+ import class Foundation. FileManager
15
+ import struct Foundation. URL
16
+
17
+ #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
18
+ import Security
19
+ #else
20
+ @_implementationOnly import CCryptoBoringSSL
21
+ #endif
22
+
11
23
protocol CertificatePolicy {
12
24
/// Validates the given certificate chain.
13
25
///
@@ -18,9 +30,362 @@ protocol CertificatePolicy {
18
30
func validate( certChain: [ Certificate ] , callback: @escaping ( Result < Bool , Error > ) -> Void )
19
31
}
20
32
21
- // TODO: actual cert policies to be implemented later
22
- struct NoopCertificatePolicy : CertificatePolicy {
23
- func validate( certChain: [ Certificate ] , callback: @escaping ( Result < Bool , Error > ) -> Void ) {
33
+ extension CertificatePolicy {
34
+ #if !(os(macOS) || os(iOS) || os(watchOS) || os(tvOS))
35
+ typealias BoringSSLVerifyCallback = @convention ( c) ( CInt , UnsafeMutablePointer < X509_STORE_CTX > ? ) -> CInt
36
+ #endif
37
+
38
+ /// Verifies the certificate.
39
+ ///
40
+ /// - Parameters:
41
+ /// - certChain: The entire certificate chain. The certificate being verified must be the first element of the array.
42
+ /// - anchorCerts: Manually specify the certificates to trust (e.g., for testing)
43
+ /// - verifyDate: Overrides the timestamp used for checking certificate expiry (e.g., for testing). By default the current time is used.
44
+ /// - queue: The `DispatchQueue` to use for async operations
45
+ /// - callback: The callback to invoke when the result is available.
46
+ func verify( certChain: [ Certificate ] ,
47
+ anchorCerts: [ Certificate ] ? = nil ,
48
+ verifyDate: Date ? = nil ,
49
+ queue: DispatchQueue ,
50
+ callback: @escaping ( Result < Bool , Error > ) -> Void ) {
51
+ guard !certChain. isEmpty else {
52
+ return callback ( . failure( CertificatePolicyError . emptyCertChain) )
53
+ }
54
+
55
+ #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
56
+ let policy = SecPolicyCreateBasicX509 ( )
57
+ let revocationPolicy = SecPolicyCreateRevocation ( kSecRevocationOCSPMethod)
58
+
59
+ var secTrust : SecTrust ?
60
+ guard SecTrustCreateWithCertificates ( certChain. map { $0. underlying } as CFArray ,
61
+ [ policy, revocationPolicy] as CFArray ,
62
+ & secTrust) == errSecSuccess,
63
+ let trust = secTrust else {
64
+ return callback ( . failure( CertificatePolicyError . certVerificationFailure) )
65
+ }
66
+
67
+ if let anchorCerts = anchorCerts {
68
+ SecTrustSetAnchorCertificates ( trust, anchorCerts. map { $0. underlying } as CFArray )
69
+ }
70
+ if let verifyDate = verifyDate {
71
+ SecTrustSetVerifyDate ( trust, verifyDate as CFDate )
72
+ }
73
+
74
+ queue. async {
75
+ // This automatically searches the user's keychain and system's store for any needed
76
+ // certificates. Passing the entire cert chain is optional and is an optimization.
77
+ SecTrustEvaluateAsyncWithError ( trust, queue) { _, isTrusted, _ in
78
+ callback ( . success( isTrusted) )
79
+ }
80
+ }
81
+ #else
82
+ // Cert chain
83
+ let x509Stack = CCryptoBoringSSL_sk_X509_new_null ( )
84
+ defer { CCryptoBoringSSL_sk_X509_free ( x509Stack) }
85
+
86
+ for i in 1 ..< certChain. count {
87
+ guard CCryptoBoringSSL_sk_X509_push ( x509Stack, certChain [ i] . underlying) > 0 else {
88
+ return callback ( . failure( CertificatePolicyError . certVerificationFailure) )
89
+ }
90
+ }
91
+
92
+ // Trusted certs
93
+ let x509Store = CCryptoBoringSSL_X509_STORE_new ( )
94
+ defer { CCryptoBoringSSL_X509_STORE_free ( x509Store) }
95
+
96
+ let x509StoreCtx = CCryptoBoringSSL_X509_STORE_CTX_new ( )
97
+ defer { CCryptoBoringSSL_X509_STORE_CTX_free ( x509StoreCtx) }
98
+
99
+ guard CCryptoBoringSSL_X509_STORE_CTX_init ( x509StoreCtx, x509Store, certChain. first!. underlying, x509Stack) == 1 else { // !-safe since certChain cannot be empty
100
+ return callback ( . failure( CertificatePolicyError . certVerificationFailure) )
101
+ }
102
+ CCryptoBoringSSL_X509_STORE_CTX_set_purpose ( x509StoreCtx, X509_PURPOSE_ANY)
103
+
104
+ anchorCerts? . forEach {
105
+ CCryptoBoringSSL_X509_STORE_add_cert ( x509Store, $0. underlying)
106
+ }
107
+
108
+ var ctxFlags : CInt = 0
109
+ if let verifyDate = verifyDate {
110
+ CCryptoBoringSSL_X509_STORE_CTX_set_time ( x509StoreCtx, 0 , numericCast ( Int ( verifyDate. timeIntervalSince1970) ) )
111
+ ctxFlags = ctxFlags | X509_V_FLAG_USE_CHECK_TIME
112
+ }
113
+ CCryptoBoringSSL_X509_STORE_CTX_set_flags ( x509StoreCtx, UInt ( ctxFlags) )
114
+
115
+ let verifyCallback : BoringSSLVerifyCallback = { result, ctx in
116
+ // Success
117
+ if result == 1 { return result }
118
+
119
+ // Custom error handling
120
+ let errorCode = CCryptoBoringSSL_X509_STORE_CTX_get_error ( ctx)
121
+ // Certs could have unknown critical extensions and cause them to be rejected.
122
+ // Instead of disabling all critical extension checks with X509_V_FLAG_IGNORE_CRITICAL
123
+ // we will just ignore this specific error.
124
+ if errorCode == X509_V_ERR_UNHANDLED_CRITICAL_EXTENSION {
125
+ return 1
126
+ }
127
+ return result
128
+ }
129
+ CCryptoBoringSSL_X509_STORE_CTX_set_verify_cb ( x509StoreCtx, verifyCallback)
130
+
131
+ guard CCryptoBoringSSL_X509_verify_cert ( x509StoreCtx) == 1 else {
132
+ // let error = CCryptoBoringSSL_X509_verify_cert_error_string(numericCast(CCryptoBoringSSL_X509_STORE_CTX_get_error(x509StoreCtx)))
133
+ return callback ( . success( false ) )
134
+ }
135
+
136
+ // TODO: OCSP
137
+ // if certChain.count >= 1 {
138
+ // // Whether cert chain can be trusted depends on OCSP result
139
+ // self.BoringSSL_OCSP_isGood(certificate: certChain[0], issuer: certChain[1], callback: callback)
140
+ // } else {
141
+ // callback(.success(true))
142
+ // }
24
143
callback ( . success( true ) )
144
+ #endif
145
+ }
146
+ }
147
+
148
+ // MARK: - Supporting methods and types
149
+
150
+ extension CertificatePolicy {
151
+ func hasExtension( oid: String , in certificate: Certificate ) throws -> Bool {
152
+ #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
153
+ guard let dict = SecCertificateCopyValues ( certificate. underlying, [ oid as CFString ] as CFArray , nil ) as? [ CFString : Any ] else {
154
+ throw CertificatePolicyError . extensionFailure
155
+ }
156
+ return !dict. isEmpty
157
+ #else
158
+ let nid = CCryptoBoringSSL_OBJ_create ( oid, " ObjectShortName " , " ObjectLongName " )
159
+ let index = CCryptoBoringSSL_X509_get_ext_by_NID ( certificate. underlying, nid, - 1 )
160
+ return index >= 0
161
+ #endif
162
+ }
163
+
164
+ func hasExtendedKeyUsage( _ usage: CertificateExtendedKeyUsage , in certificate: Certificate ) throws -> Bool {
165
+ #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
166
+ guard let dict = SecCertificateCopyValues ( certificate. underlying, [ kSecOIDExtendedKeyUsage] as CFArray , nil ) as? [ CFString : Any ] else {
167
+ throw CertificatePolicyError . extensionFailure
168
+ }
169
+ guard let usageDict = dict [ kSecOIDExtendedKeyUsage] as? [ CFString : Any ] ,
170
+ let usages = usageDict [ kSecPropertyKeyValue] as? [ Data ] else {
171
+ return false
172
+ }
173
+ return usages. first ( where: { $0 == usage. data } ) != nil
174
+ #else
175
+ let eku = CCryptoBoringSSL_X509_get_extended_key_usage ( certificate. underlying)
176
+ return eku & UInt32 ( usage. flag) > 0
177
+ #endif
178
+ }
179
+
180
+ /// Checks that the certificate supports OCSP. This **must** be done before calling `verify` to ensure
181
+ /// the necessary properties are in place to trigger revocation check.
182
+ func supportsOCSP( certificate: Certificate ) throws -> Bool {
183
+ #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
184
+ // Check that certificate has "Certificate Authority Information Access" extension and includes OCSP as access method.
185
+ // The actual revocation check will be done by the Security framework in `verify`.
186
+ guard let dict = SecCertificateCopyValues ( certificate. underlying, [ kSecOIDAuthorityInfoAccess] as CFArray , nil ) as? [ CFString : Any ] else { // ignore error
187
+ throw CertificatePolicyError . extensionFailure
188
+ }
189
+ guard let infoAccessDict = dict [ kSecOIDAuthorityInfoAccess] as? [ CFString : Any ] ,
190
+ let infoAccessValue = infoAccessDict [ kSecPropertyKeyValue] as? [ [ CFString : Any ] ] else {
191
+ return false
192
+ }
193
+ return infoAccessValue. first ( where: { valueDict in valueDict [ kSecPropertyKeyValue] as? String == " 1.3.6.1.5.5.7.48.1 " } ) != nil
194
+ #else
195
+ // Check that there is at least one OCSP responder URL, in which case OCSP check will take place in `verify`.
196
+ let ocspURLs = CCryptoBoringSSL_X509_get1_ocsp ( certificate. underlying)
197
+ defer { CCryptoBoringSSL_sk_OPENSSL_STRING_free ( ocspURLs) }
198
+
199
+ return CCryptoBoringSSL_sk_OPENSSL_STRING_num ( ocspURLs) > 0
200
+ #endif
201
+ }
202
+ }
203
+
204
+ enum CertificateExtendedKeyUsage {
205
+ case codeSigning
206
+
207
+ #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
208
+ var data : Data {
209
+ switch self {
210
+ case . codeSigning:
211
+ // https://stackoverflow.com/questions/49489591/how-to-extract-or-compare-ksecpropertykeyvalue-from-seccertificate
212
+ // https://github.com/google/der-ascii/blob/cd91cb85bb0d71e4611856e4f76f5110609d7e42/cmd/der2ascii/oid_names.go#L100
213
+ return Data ( [ 0x2B , 0x06 , 0x01 , 0x05 , 0x05 , 0x07 , 0x03 , 0x03 ] )
214
+ }
215
+ }
216
+
217
+ #else
218
+ var flag : CInt {
219
+ switch self {
220
+ case . codeSigning:
221
+ // https://www.openssl.org/docs/man1.1.0/man3/X509_get_extension_flags.html
222
+ return XKU_CODE_SIGN
223
+ }
25
224
}
225
+ #endif
226
+ }
227
+
228
+ extension CertificatePolicy {
229
+ static func loadCerts( at directory: URL ) -> [ Certificate ] {
230
+ var certs = [ Certificate] ( )
231
+ if let enumerator = FileManager . default. enumerator ( at: directory, includingPropertiesForKeys: nil ) {
232
+ for case let fileURL as URL in enumerator {
233
+ do {
234
+ certs. append ( try Certificate ( derEncoded: Data ( contentsOf: fileURL) ) )
235
+ } catch {
236
+ // Skip cert if problematic
237
+ }
238
+ }
239
+ }
240
+ return certs
241
+ }
242
+ }
243
+
244
+ enum CertificatePolicyError : Error {
245
+ case emptyCertChain
246
+ case certVerificationFailure
247
+ case extensionFailure
248
+ // case ocspFailure
249
+ }
250
+
251
+ // MARK: - Certificate policies
252
+
253
+ /// Default policy for validating certificates used to sign package collections.
254
+ ///
255
+ /// Certificates must satisfy these conditions:
256
+ /// - The timestamp at which signing/verification is done must fall within the signing certificate’s validity period.
257
+ /// - The certificate’s “Extended Key Usage” extension must include “Code Signing”.
258
+ /// - The certificate must use either 256-bit EC (recommended) or 2048-bit RSA key.
259
+ /// - The certificate must not be revoked. The certificate authority must support OCSP, which means the certificate must have the
260
+ /// "Certificate Authority Information Access" extension that includes OCSP as a method, specifying the responder’s URL.
261
+ /// - The certificate chain is valid and root certificate must be trusted.
262
+ struct DefaultCertificatePolicy : CertificatePolicy {
263
+ let trustedRoots : [ Certificate ] ?
264
+ let expectedSubjectUserID : String ?
265
+
266
+ let queue : DispatchQueue
267
+
268
+ /// Initializes a `DefaultCertificatePolicy`.
269
+ /// - Parameters:
270
+ /// - trustedRootCertsDir: On Apple platforms, all root certificates that come preinstalled with the OS are automatically trusted.
271
+ /// Users may specify additional certificates to trust by placing them in `trustedRootCertsDir` and
272
+ /// configure the signing tool or SwiftPM to use it. On non-Apple platforms, only trust root certificates in
273
+ /// `trustedRootCertsDir` are trusted.
274
+ /// - expectedSubjectUserID: The subject user ID that must match if specified.
275
+ /// - queue: The `DispatchQueue` to perform async operations.
276
+ init ( trustedRootCertsDir: URL ? = nil , expectedSubjectUserID: String ? = nil , queue: DispatchQueue = DispatchQueue . global ( ) ) {
277
+ self . trustedRoots = trustedRootCertsDir. map { Self . loadCerts ( at: $0) }
278
+ self . expectedSubjectUserID = expectedSubjectUserID
279
+ self . queue = queue
280
+ }
281
+
282
+ func validate( certChain: [ Certificate ] , callback: @escaping ( Result < Bool , Error > ) -> Void ) {
283
+ guard !certChain. isEmpty else {
284
+ return callback ( . failure( CertificatePolicyError . emptyCertChain) )
285
+ }
286
+
287
+ do {
288
+ // Check if subject user ID matches
289
+ if let expectedSubjectUserID = self . expectedSubjectUserID {
290
+ guard try certChain [ 0 ] . subject ( ) . userID == expectedSubjectUserID else {
291
+ return callback ( . success( false ) )
292
+ }
293
+ }
294
+
295
+ // Must be a code signing certificate
296
+ guard try self . hasExtendedKeyUsage ( . codeSigning, in: certChain [ 0 ] ) else {
297
+ return callback ( . success( false ) )
298
+ }
299
+ // Must support OCSP
300
+ guard try self . supportsOCSP ( certificate: certChain [ 0 ] ) else {
301
+ return callback ( . success( false ) )
302
+ }
303
+
304
+ // Verify the cert chain - if it is trusted then cert chain is valid
305
+ self . verify ( certChain: certChain, anchorCerts: self . trustedRoots, queue: self . queue, callback: callback)
306
+ } catch {
307
+ return callback ( . failure( error) )
308
+ }
309
+ }
310
+ }
311
+
312
+ /// Policy for validating developer.apple.com certificates.
313
+ ///
314
+ /// This has the same requirements as `DefaultCertificatePolicy` plus additional
315
+ /// marker extensions for Apple Distribution certifiications.
316
+ struct AppleDeveloperCertificatePolicy : CertificatePolicy {
317
+ private static let expectedCertChainLength = 3
318
+ private static let appleDistributionIOSMarker = " 1.2.840.113635.100.6.1.4 "
319
+ private static let appleDistributionMacOSMarker = " 1.2.840.113635.100.6.1.7 "
320
+ private static let appleIntermediateMarker = " 1.2.840.113635.100.6.2.1 "
321
+
322
+ let trustedRoots : [ Certificate ] ?
323
+ let expectedSubjectUserID : String ?
324
+
325
+ let queue : DispatchQueue
326
+
327
+ /// Initializes a `AppleDeveloperCertificatePolicy`.
328
+ /// - Parameters:
329
+ /// - trustedRootCertsDir: On Apple platforms, all root certificates that come preinstalled with the OS are automatically trusted.
330
+ /// Users may specify additional certificates to trust by placing them in `trustedRootCertsDir` and
331
+ /// configure the signing tool or SwiftPM to use it. On non-Apple platforms, only trust root certificates in
332
+ /// `trustedRootCertsDir` are trusted.
333
+ /// - expectedSubjectUserID: The subject user ID that must match if specified.
334
+ /// - queue: The `DispatchQueue` to perform async operations.
335
+ init ( trustedRootCertsDir: URL ? = nil , expectedSubjectUserID: String ? = nil , queue: DispatchQueue = DispatchQueue . global ( ) ) {
336
+ self . trustedRoots = trustedRootCertsDir. map { Self . loadCerts ( at: $0) }
337
+ self . expectedSubjectUserID = expectedSubjectUserID
338
+ self . queue = queue
339
+ }
340
+
341
+ func validate( certChain: [ Certificate ] , callback: @escaping ( Result < Bool , Error > ) -> Void ) {
342
+ guard !certChain. isEmpty else {
343
+ return callback ( . failure( CertificatePolicyError . emptyCertChain) )
344
+ }
345
+ // developer.apple.com cert chain is always 3-long
346
+ guard certChain. count == Self . expectedCertChainLength else {
347
+ return callback ( . success( false ) )
348
+ }
349
+
350
+ do {
351
+ // Check if subject user ID matches
352
+ if let expectedSubjectUserID = self . expectedSubjectUserID {
353
+ guard try certChain [ 0 ] . subject ( ) . userID == expectedSubjectUserID else {
354
+ return callback ( . success( false ) )
355
+ }
356
+ }
357
+
358
+ // Check marker extensions (certificates issued post WWDC 2019 have both extensions but earlier ones have just one depending on platform)
359
+ guard try ( self . hasExtension ( oid: Self . appleDistributionIOSMarker, in: certChain [ 0 ] ) || self . hasExtension ( oid: Self . appleDistributionMacOSMarker, in: certChain [ 0 ] ) ) else {
360
+ return callback ( . success( false ) )
361
+ }
362
+ guard try self . hasExtension ( oid: Self . appleIntermediateMarker, in: certChain [ 1 ] ) else {
363
+ return callback ( . success( false ) )
364
+ }
365
+ // Must be a code signing certificate
366
+ guard try self . hasExtendedKeyUsage ( . codeSigning, in: certChain [ 0 ] ) else {
367
+ return callback ( . success( false ) )
368
+ }
369
+ // Must support OCSP
370
+ guard try self . supportsOCSP ( certificate: certChain [ 0 ] ) else {
371
+ return callback ( . success( false ) )
372
+ }
373
+
374
+ // Verify the cert chain - if it is trusted then cert chain is valid
375
+ self . verify ( certChain: certChain, anchorCerts: self . trustedRoots, queue: self . queue, callback: callback)
376
+ } catch {
377
+ return callback ( . failure( error) )
378
+ }
379
+ }
380
+ }
381
+
382
+ public enum CertificatePolicyKey : Equatable , Hashable {
383
+ case `default`( subjectUserID: String ? )
384
+ case appleDistribution( subjectUserID: String ? )
385
+
386
+ /// For internal-use only
387
+ case custom
388
+
389
+ public static let `default` = CertificatePolicyKey . default ( subjectUserID: nil )
390
+ public static let appleDistribution = CertificatePolicyKey . appleDistribution ( subjectUserID: nil )
26
391
}
0 commit comments