Skip to content

Commit fd14a63

Browse files
authored
[Collections] Signing (apple, 3): certificate validations (#3269)
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 df1e396 commit fd14a63

File tree

7 files changed

+1020
-32
lines changed

7 files changed

+1020
-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+
TSCBasic
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: 312 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,306 @@ 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]?,
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+
23282
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+
}
25323
}
26324
}
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

Comments
 (0)