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
+ import TSCBasic
18
+
19
+ #if canImport(Security)
20
+ import Security
21
+ #endif
22
+
11
23
protocol CertificatePolicy {
12
24
/// Validates the given certificate chain.
13
25
///
@@ -18,9 +30,306 @@ protocol CertificatePolicy {
18
30
func validate( certChain: [ Certificate ] , callback: @escaping ( Result < Void , Error > ) -> Void )
19
31
}
20
32
21
- // TODO: actual cert policies to be implemented later
22
- struct NoopCertificatePolicy : CertificatePolicy {
33
+ extension CertificatePolicy {
34
+ /// Verifies the certificate.
35
+ ///
36
+ /// - Parameters:
37
+ /// - certChain: The entire certificate chain. The certificate being verified must be the first element of the array.
38
+ /// - anchorCerts: On Apple platforms, these are root certificates to trust **in addition** to the operating system's trust store.
39
+ /// On other platforms, these are the **only** root certificates to be trusted.
40
+ /// - verifyDate: Overrides the timestamp used for checking certificate expiry (e.g., for testing). By default the current time is used.
41
+ /// - diagnosticsEngine: The `DiagnosticsEngine` for emitting warnings and errors
42
+ /// - callbackQueue: The `DispatchQueue` to use for callbacks
43
+ /// - callback: The callback to invoke when the result is available.
44
+ func verify( certChain: [ Certificate ] ,
45
+ anchorCerts: [ Certificate ] ? ,
46
+ verifyDate: Date ? = nil ,
47
+ diagnosticsEngine: DiagnosticsEngine ,
48
+ callbackQueue: DispatchQueue ,
49
+ callback: @escaping ( Result < Void , Error > ) -> Void ) {
50
+ let wrappedCallback : ( Result < Void , Error > ) -> Void = { result in callbackQueue. async { callback ( result) } }
51
+
52
+ guard !certChain. isEmpty else {
53
+ return wrappedCallback ( . failure( CertificatePolicyError . emptyCertChain) )
54
+ }
55
+
56
+ #if canImport(Security)
57
+ let policy = SecPolicyCreateBasicX509 ( )
58
+ let revocationPolicy = SecPolicyCreateRevocation ( kSecRevocationOCSPMethod)
59
+
60
+ var secTrust : SecTrust ?
61
+ guard SecTrustCreateWithCertificates ( certChain. map { $0. underlying } as CFArray ,
62
+ [ policy, revocationPolicy] as CFArray ,
63
+ & secTrust) == errSecSuccess,
64
+ let trust = secTrust else {
65
+ return wrappedCallback ( . failure( CertificatePolicyError . trustSetupFailure) )
66
+ }
67
+
68
+ if let anchorCerts = anchorCerts {
69
+ SecTrustSetAnchorCertificates ( trust, anchorCerts. map { $0. underlying } as CFArray )
70
+ }
71
+ if let verifyDate = verifyDate {
72
+ SecTrustSetVerifyDate ( trust, verifyDate as CFDate )
73
+ }
74
+
75
+ callbackQueue. async {
76
+ // This automatically searches the user's keychain and system's store for any needed
77
+ // certificates. Passing the entire cert chain is optional and is an optimization.
78
+ SecTrustEvaluateAsyncWithError ( trust, callbackQueue) { _, isTrusted, _ in
79
+ guard isTrusted else {
80
+ return wrappedCallback ( . failure( CertificatePolicyError . invalidCertChain) )
81
+ }
82
+ wrappedCallback ( . success( ( ) ) )
83
+ }
84
+ }
85
+ #else
86
+ fatalError ( " Not implemented: \( #function) " )
87
+ #endif
88
+ }
89
+ }
90
+
91
+ // MARK: - Supporting methods and types
92
+
93
+ extension CertificatePolicy {
94
+ func hasExtension( oid: String , in certificate: Certificate ) throws -> Bool {
95
+ #if canImport(Security)
96
+ guard let dict = SecCertificateCopyValues ( certificate. underlying, [ oid as CFString ] as CFArray , nil ) as? [ CFString : Any ] else {
97
+ throw CertificatePolicyError . extensionFailure
98
+ }
99
+ return !dict. isEmpty
100
+ #else
101
+ fatalError ( " Not implemented: \( #function) " )
102
+ #endif
103
+ }
104
+
105
+ func hasExtendedKeyUsage( _ usage: CertificateExtendedKeyUsage , in certificate: Certificate ) throws -> Bool {
106
+ #if canImport(Security)
107
+ guard let dict = SecCertificateCopyValues ( certificate. underlying, [ kSecOIDExtendedKeyUsage] as CFArray , nil ) as? [ CFString : Any ] else {
108
+ throw CertificatePolicyError . extensionFailure
109
+ }
110
+ guard let usageDict = dict [ kSecOIDExtendedKeyUsage] as? [ CFString : Any ] ,
111
+ let usages = usageDict [ kSecPropertyKeyValue] as? [ Data ] else {
112
+ return false
113
+ }
114
+ return usages. first ( where: { $0 == usage. data } ) != nil
115
+ #else
116
+ fatalError ( " Not implemented: \( #function) " )
117
+ #endif
118
+ }
119
+
120
+ /// Checks that the certificate supports OCSP. This **must** be done before calling `verify` to ensure
121
+ /// the necessary properties are in place to trigger revocation check.
122
+ func supportsOCSP( certificate: Certificate ) throws -> Bool {
123
+ #if canImport(Security)
124
+ // Check that certificate has "Certificate Authority Information Access" extension and includes OCSP as access method.
125
+ // The actual revocation check will be done by the Security framework in `verify`.
126
+ guard let dict = SecCertificateCopyValues ( certificate. underlying, [ kSecOIDAuthorityInfoAccess] as CFArray , nil ) as? [ CFString : Any ] else { // ignore error
127
+ throw CertificatePolicyError . extensionFailure
128
+ }
129
+ guard let infoAccessDict = dict [ kSecOIDAuthorityInfoAccess] as? [ CFString : Any ] ,
130
+ let infoAccessValue = infoAccessDict [ kSecPropertyKeyValue] as? [ [ CFString : Any ] ] else {
131
+ return false
132
+ }
133
+ return infoAccessValue. first ( where: { valueDict in valueDict [ kSecPropertyKeyValue] as? String == " 1.3.6.1.5.5.7.48.1 " } ) != nil
134
+ #else
135
+ fatalError ( " Not implemented: \( #function) " )
136
+ #endif
137
+ }
138
+ }
139
+
140
+ enum CertificateExtendedKeyUsage {
141
+ case codeSigning
142
+
143
+ #if canImport(Security)
144
+ var data : Data {
145
+ switch self {
146
+ case . codeSigning:
147
+ // https://stackoverflow.com/questions/49489591/how-to-extract-or-compare-ksecpropertykeyvalue-from-seccertificate
148
+ // https://github.com/google/der-ascii/blob/cd91cb85bb0d71e4611856e4f76f5110609d7e42/cmd/der2ascii/oid_names.go#L100
149
+ return Data ( [ 0x2B , 0x06 , 0x01 , 0x05 , 0x05 , 0x07 , 0x03 , 0x03 ] )
150
+ }
151
+ }
152
+ #endif
153
+ }
154
+
155
+ extension CertificatePolicy {
156
+ static func loadCerts( at directory: URL , diagnosticsEngine: DiagnosticsEngine ) -> [ Certificate ] {
157
+ var certs = [ Certificate] ( )
158
+ if let enumerator = FileManager . default. enumerator ( at: directory, includingPropertiesForKeys: nil ) {
159
+ for case let fileURL as URL in enumerator {
160
+ do {
161
+ certs. append ( try Certificate ( derEncoded: Data ( contentsOf: fileURL) ) )
162
+ } catch {
163
+ diagnosticsEngine. emit ( warning: " The certificate \( fileURL) is invalid: \( error) " )
164
+ }
165
+ }
166
+ }
167
+ return certs
168
+ }
169
+ }
170
+
171
+ enum CertificatePolicyError : Error , Equatable {
172
+ case emptyCertChain
173
+ case trustSetupFailure
174
+ case invalidCertChain
175
+ case subjectUserIDMismatch
176
+ case codeSigningCertRequired
177
+ case ocspSupportRequired
178
+ case unexpectedCertChainLength
179
+ case missingRequiredExtension
180
+ case extensionFailure
181
+ // case ocspFailure
182
+ }
183
+
184
+ // MARK: - Certificate policies
185
+
186
+ /// Default policy for validating certificates used to sign package collections.
187
+ ///
188
+ /// Certificates must satisfy these conditions:
189
+ /// - The timestamp at which signing/verification is done must fall within the signing certificate’s validity period.
190
+ /// - The certificate’s “Extended Key Usage” extension must include “Code Signing”.
191
+ /// - The certificate must use either 256-bit EC (recommended) or 2048-bit RSA key.
192
+ /// - The certificate must not be revoked. The certificate authority must support OCSP, which means the certificate must have the
193
+ /// "Certificate Authority Information Access" extension that includes OCSP as a method, specifying the responder’s URL.
194
+ /// - The certificate chain is valid and root certificate must be trusted.
195
+ struct DefaultCertificatePolicy : CertificatePolicy {
196
+ let trustedRoots : [ Certificate ] ?
197
+ let expectedSubjectUserID : String ?
198
+
199
+ private let callbackQueue : DispatchQueue
200
+ private let diagnosticsEngine : DiagnosticsEngine
201
+
202
+ /// Initializes a `DefaultCertificatePolicy`.
203
+ /// - Parameters:
204
+ /// - trustedRootCertsDir: On Apple platforms, all root certificates that come preinstalled with the OS are automatically trusted.
205
+ /// Users may specify additional certificates to trust by placing them in `trustedRootCertsDir` and
206
+ /// configure the signing tool or SwiftPM to use it. On non-Apple platforms, only trust root certificates in
207
+ /// `trustedRootCertsDir` are trusted.
208
+ /// - expectedSubjectUserID: The subject user ID that must match if specified.
209
+ /// - callbackQueue: The `DispatchQueue` to use for callbacks
210
+ /// - diagnosticsEngine: The `DiagnosticsEngine` for emitting warnings and errors.
211
+ init ( trustedRootCertsDir: URL ? = nil , expectedSubjectUserID: String ? = nil , callbackQueue: DispatchQueue , diagnosticsEngine: DiagnosticsEngine ) {
212
+ self . trustedRoots = trustedRootCertsDir. map { Self . loadCerts ( at: $0, diagnosticsEngine: diagnosticsEngine) }
213
+ self . expectedSubjectUserID = expectedSubjectUserID
214
+ self . callbackQueue = callbackQueue
215
+ self . diagnosticsEngine = diagnosticsEngine
216
+ }
217
+
218
+ func validate( certChain: [ Certificate ] , callback: @escaping ( Result < Void , Error > ) -> Void ) {
219
+ let wrappedCallback : ( Result < Void , Error > ) -> Void = { result in self . callbackQueue. async { callback ( result) } }
220
+
221
+ guard !certChain. isEmpty else {
222
+ return wrappedCallback ( . failure( CertificatePolicyError . emptyCertChain) )
223
+ }
224
+
225
+ do {
226
+ // Check if subject user ID matches
227
+ if let expectedSubjectUserID = self . expectedSubjectUserID {
228
+ guard try certChain [ 0 ] . subject ( ) . userID == expectedSubjectUserID else {
229
+ return wrappedCallback ( . failure( CertificatePolicyError . subjectUserIDMismatch) )
230
+ }
231
+ }
232
+
233
+ // Must be a code signing certificate
234
+ guard try self . hasExtendedKeyUsage ( . codeSigning, in: certChain [ 0 ] ) else {
235
+ return wrappedCallback ( . failure( CertificatePolicyError . codeSigningCertRequired) )
236
+ }
237
+ // Must support OCSP
238
+ guard try self . supportsOCSP ( certificate: certChain [ 0 ] ) else {
239
+ return wrappedCallback ( . failure( CertificatePolicyError . ocspSupportRequired) )
240
+ }
241
+
242
+ // Verify the cert chain - if it is trusted then cert chain is valid
243
+ self . verify ( certChain: certChain, anchorCerts: self . trustedRoots, diagnosticsEngine: self . diagnosticsEngine, callbackQueue: self . callbackQueue, callback: callback)
244
+ } catch {
245
+ return wrappedCallback ( . failure( error) )
246
+ }
247
+ }
248
+ }
249
+
250
+ /// Policy for validating developer.apple.com certificates.
251
+ ///
252
+ /// This has the same requirements as `DefaultCertificatePolicy` plus additional
253
+ /// marker extensions for Apple Distribution certifiications.
254
+ struct AppleDeveloperCertificatePolicy : CertificatePolicy {
255
+ private static let expectedCertChainLength = 3
256
+ private static let appleDistributionIOSMarker = " 1.2.840.113635.100.6.1.4 "
257
+ private static let appleDistributionMacOSMarker = " 1.2.840.113635.100.6.1.7 "
258
+ private static let appleIntermediateMarker = " 1.2.840.113635.100.6.2.1 "
259
+
260
+ let trustedRoots : [ Certificate ] ?
261
+ let expectedSubjectUserID : String ?
262
+
263
+ private let callbackQueue : DispatchQueue
264
+ private let diagnosticsEngine : DiagnosticsEngine
265
+
266
+ /// Initializes a `AppleDeveloperCertificatePolicy`.
267
+ /// - Parameters:
268
+ /// - trustedRootCertsDir: On Apple platforms, all root certificates that come preinstalled with the OS are automatically trusted.
269
+ /// Users may specify additional certificates to trust by placing them in `trustedRootCertsDir` and
270
+ /// configure the signing tool or SwiftPM to use it. On non-Apple platforms, only trust root certificates in
271
+ /// `trustedRootCertsDir` are trusted.
272
+ /// - expectedSubjectUserID: The subject user ID that must match if specified.
273
+ /// - callbackQueue: The `DispatchQueue` to use for callbacks
274
+ /// - diagnosticsEngine: The `DiagnosticsEngine` for emitting warnings and errors.
275
+ init ( trustedRootCertsDir: URL ? = nil , expectedSubjectUserID: String ? = nil , callbackQueue: DispatchQueue , diagnosticsEngine: DiagnosticsEngine ) {
276
+ self . trustedRoots = trustedRootCertsDir. map { Self . loadCerts ( at: $0, diagnosticsEngine: diagnosticsEngine) }
277
+ self . expectedSubjectUserID = expectedSubjectUserID
278
+ self . callbackQueue = callbackQueue
279
+ self . diagnosticsEngine = diagnosticsEngine
280
+ }
281
+
23
282
func validate( certChain: [ Certificate ] , callback: @escaping ( Result < Void , Error > ) -> Void ) {
24
- callback ( . success( ( ) ) )
283
+ let wrappedCallback : ( Result < Void , Error > ) -> Void = { result in self . callbackQueue. async { callback ( result) } }
284
+
285
+ guard !certChain. isEmpty else {
286
+ return wrappedCallback ( . failure( CertificatePolicyError . emptyCertChain) )
287
+ }
288
+ // developer.apple.com cert chain is always 3-long
289
+ guard certChain. count == Self . expectedCertChainLength else {
290
+ return wrappedCallback ( . failure( CertificatePolicyError . unexpectedCertChainLength) )
291
+ }
292
+
293
+ do {
294
+ // Check if subject user ID matches
295
+ if let expectedSubjectUserID = self . expectedSubjectUserID {
296
+ guard try certChain [ 0 ] . subject ( ) . userID == expectedSubjectUserID else {
297
+ return wrappedCallback ( . failure( CertificatePolicyError . subjectUserIDMismatch) )
298
+ }
299
+ }
300
+
301
+ // Check marker extensions (certificates issued post WWDC 2019 have both extensions but earlier ones have just one depending on platform)
302
+ guard try ( self . hasExtension ( oid: Self . appleDistributionIOSMarker, in: certChain [ 0 ] ) || self . hasExtension ( oid: Self . appleDistributionMacOSMarker, in: certChain [ 0 ] ) ) else {
303
+ return wrappedCallback ( . failure( CertificatePolicyError . missingRequiredExtension) )
304
+ }
305
+ guard try self . hasExtension ( oid: Self . appleIntermediateMarker, in: certChain [ 1 ] ) else {
306
+ return wrappedCallback ( . failure( CertificatePolicyError . missingRequiredExtension) )
307
+ }
308
+
309
+ // Must be a code signing certificate
310
+ guard try self . hasExtendedKeyUsage ( . codeSigning, in: certChain [ 0 ] ) else {
311
+ return wrappedCallback ( . failure( CertificatePolicyError . codeSigningCertRequired) )
312
+ }
313
+ // Must support OCSP
314
+ guard try self . supportsOCSP ( certificate: certChain [ 0 ] ) else {
315
+ return wrappedCallback ( . failure( CertificatePolicyError . ocspSupportRequired) )
316
+ }
317
+
318
+ // Verify the cert chain - if it is trusted then cert chain is valid
319
+ self . verify ( certChain: certChain, anchorCerts: self . trustedRoots, diagnosticsEngine: self . diagnosticsEngine, callbackQueue: self . callbackQueue, callback: callback)
320
+ } catch {
321
+ return wrappedCallback ( . failure( error) )
322
+ }
25
323
}
26
324
}
325
+
326
+ public enum CertificatePolicyKey : Equatable , Hashable {
327
+ case `default`( subjectUserID: String ? )
328
+ case appleDistribution( subjectUserID: String ? )
329
+
330
+ /// For internal-use only
331
+ case custom
332
+
333
+ public static let `default` = CertificatePolicyKey . default ( subjectUserID: nil )
334
+ public static let appleDistribution = CertificatePolicyKey . appleDistribution ( subjectUserID: nil )
335
+ }
0 commit comments