Skip to content

Commit 84961bd

Browse files
committed
[Collections] Signing (apple, 3): certificate validations
This is part 3 of a series of PRs to support package collection signing on **Apple** platforms. Originally #3245. Depends on #3264 Modifications: - Add certificate policies to validate collection-signing certificates. - Wire up `PackageCollectionSigning` with certificate policies.
1 parent 23bb777 commit 84961bd

File tree

7 files changed

+1008
-32
lines changed

7 files changed

+1008
-32
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ let package = Package(
156156
.target(
157157
/** Package collections signing */
158158
name: "PackageCollectionsSigning",
159-
dependencies: ["PackageCollectionsModel", "Crypto"]),
159+
dependencies: ["PackageCollectionsModel", "Crypto", "Basics"]),
160160

161161
.target(
162162
/** Data structures and support for package collections */

Sources/PackageCollectionsSigning/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ add_library(PackageCollectionsSigning
3030
Utilities/Base64URL.swift
3131
Utilities/Utilities.swift)
3232
target_link_libraries(PackageCollectionsSigning PUBLIC
33+
Basics
3334
PackageCollectionsModel
35+
TSCBasics
3436
$<$<NOT:$<PLATFORM_ID:Darwin>>:dispatch>
3537
$<$<NOT:$<PLATFORM_ID:Darwin>>:Foundation>)
3638
target_link_libraries(PackageCollectionsSigning PRIVATE

Sources/PackageCollectionsSigning/Certificate/CertificatePolicy.swift

Lines changed: 306 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+
import TSCBasic
18+
19+
#if canImport(Security)
20+
import Security
21+
#endif
22+
1123
protocol CertificatePolicy {
1224
/// Validates the given certificate chain.
1325
///
@@ -18,9 +30,300 @@ protocol CertificatePolicy {
1830
func validate(certChain: [Certificate], callback: @escaping (Result<Void, Error>) -> Void)
1931
}
2032

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+
23278
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+
}
25317
}
26318
}
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

Comments
 (0)