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