Skip to content

Commit 9437c33

Browse files
committed
[Collections] Signing (part 3): certificate validations
This is part 3 of a series of PRs to support package collection signing. Depends on [part 2](#3242) Modifications: - Certificate policies to validate collection signing certificates. OCSP support on non-Apple platforms to come in part 4. - Wire up `PackageCollectionSigning` with certificate policies.
1 parent 8c32486 commit 9437c33

File tree

6 files changed

+953
-28
lines changed

6 files changed

+953
-28
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ let package = Package(
161161
.target(
162162
/** Package collections signing */
163163
name: "PackageCollectionsSigning",
164-
dependencies: ["PackageCollectionsModel", "Crypto"]),
164+
dependencies: ["PackageCollectionsModel", "Crypto", "Basics"]),
165165

166166
// MARK: Package Manager Functionality
167167

Sources/PackageCollectionsSigning/Certificate/CertificatePolicy.swift

Lines changed: 368 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@
88
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
99
*/
1010

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+
1123
protocol CertificatePolicy {
1224
/// Validates the given certificate chain.
1325
///
@@ -18,9 +30,362 @@ protocol CertificatePolicy {
1830
func validate(certChain: [Certificate], callback: @escaping (Result<Bool, Error>) -> Void)
1931
}
2032

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+
// }
24143
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+
}
25224
}
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)
26391
}

0 commit comments

Comments
 (0)