Skip to content

Commit d406b49

Browse files
committed
[Collections] Signing (apple, 4): signature validation
This is part 4 of a series of PRs to support package collection signing on Apple platforms. Originally #3252. Depends on #3269. Modifications: - Add `PackageCollectionsSigning` dependency to `PackageCollections` module - Verify collection signature in `JSONPackageCollectionProvider`
1 parent fe7242e commit d406b49

File tree

11 files changed

+319
-222
lines changed

11 files changed

+319
-222
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
/** Data structures and support for package collections */
163163
name: "PackageCollections",
164-
dependencies: ["SwiftToolsSupport-auto", "Basics", "PackageModel", "SourceControl", "PackageCollectionsModel"]),
164+
dependencies: ["SwiftToolsSupport-auto", "Basics", "PackageModel", "SourceControl", "PackageCollectionsModel", "PackageCollectionsSigning"]),
165165

166166
// MARK: Package Manager Functionality
167167

Sources/PackageCollections/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ target_link_libraries(PackageCollections PUBLIC
3232
TSCBasic
3333
TSCUtility
3434
Basics
35+
Crypto
36+
PackageCollectionsModel
37+
PackageCollectionsSigning
3538
PackageModel
3639
SourceControl)
3740
# NOTE(compnerd) workaround for CMake not setting up include flags yet

Sources/PackageCollections/Providers/JSONPackageCollectionProvider.swift

Lines changed: 56 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010

1111
import Basics
1212
import Dispatch
13+
import struct Foundation.Data
1314
import struct Foundation.Date
1415
import class Foundation.JSONDecoder
1516
import struct Foundation.URL
1617

1718
import PackageCollectionsModel
19+
import PackageCollectionsSigning
1820
import PackageModel
1921
import SourceControl
2022
import TSCBasic
@@ -27,13 +29,21 @@ struct JSONPackageCollectionProvider: PackageCollectionProvider {
2729
private let httpClient: HTTPClient
2830
private let decoder: JSONDecoder
2931
private let validator: JSONModel.Validator
32+
private let signatureValidator: PackageCollectionSignatureValidator
3033

31-
init(configuration: Configuration = .init(), httpClient: HTTPClient? = nil, diagnosticsEngine: DiagnosticsEngine? = nil) {
34+
init(configuration: Configuration = .init(),
35+
httpClient: HTTPClient? = nil,
36+
signatureValidator: PackageCollectionSignatureValidator? = nil,
37+
diagnosticsEngine: DiagnosticsEngine? = nil) {
3238
self.configuration = configuration
3339
self.diagnosticsEngine = diagnosticsEngine
3440
self.httpClient = httpClient ?? Self.makeDefaultHTTPClient(diagnosticsEngine: diagnosticsEngine)
3541
self.decoder = JSONDecoder.makeWithDefaults()
3642
self.validator = JSONModel.Validator(configuration: configuration.validator)
43+
self.signatureValidator = signatureValidator ?? PackageCollectionSigning(
44+
trustedRootCertsDir: configuration.trustedRootCertsDir,
45+
diagnosticsEngine: diagnosticsEngine
46+
)
3747
}
3848

3949
func get(_ source: Model.CollectionSource, callback: @escaping (Result<Model.Collection, Error>) -> Void) {
@@ -49,14 +59,9 @@ struct JSONPackageCollectionProvider: PackageCollectionProvider {
4959
if let absolutePath = source.absolutePath {
5060
do {
5161
let fileContents = try localFileSystem.readFileContents(absolutePath)
52-
let collection: JSONModel.Collection = try fileContents.withData { data in
53-
do {
54-
return try self.decoder.decode(JSONModel.Collection.self, from: data)
55-
} catch {
56-
throw Errors.invalidJSON(error)
57-
}
62+
return fileContents.withData { data in
63+
self.decodeAndRunSignatureCheck(source: source, data: data, certPolicyKey: .default, callback: callback)
5864
}
59-
return callback(self.makeCollection(from: collection, source: source, signature: nil))
6065
} catch {
6166
return callback(.failure(error))
6267
}
@@ -97,33 +102,40 @@ struct JSONPackageCollectionProvider: PackageCollectionProvider {
97102
return callback(.failure(Errors.invalidResponse("Body is empty")))
98103
}
99104

100-
do {
101-
// parse json and construct result
102-
do {
103-
// This fails if "signature" is missing
104-
let signature = try JSONModel.SignedCollection.signature(from: body, using: self.decoder)
105-
// TODO: Check collection's signature
106-
// If signature is
107-
// a. valid: process the collection; set isSigned=true
108-
// b. invalid: includes expired cert, untrusted cert, signature-payload mismatch => return error
109-
let collection = try JSONModel.SignedCollection.collection(from: body, using: self.decoder)
110-
callback(self.makeCollection(from: collection, source: source, signature: Model.SignatureData(from: signature)))
111-
} catch {
112-
// Collection is not signed
113-
guard let collection = try response.decodeBody(JSONModel.Collection.self, using: self.decoder) else {
114-
return callback(.failure(Errors.invalidResponse("Invalid body")))
115-
}
116-
callback(self.makeCollection(from: collection, source: source, signature: nil))
117-
}
118-
} catch {
119-
callback(.failure(Errors.invalidJSON(error)))
120-
}
105+
let certPolicyKey = self.configuration.certificatePolicyKey(for: source) ?? .default
106+
self.decodeAndRunSignatureCheck(source: source, data: body, certPolicyKey: certPolicyKey, callback: callback)
121107
}
122108
}
123109
}
124110
}
125111
}
126112

113+
private func decodeAndRunSignatureCheck(source: Model.CollectionSource,
114+
data: Data,
115+
certPolicyKey: CertificatePolicyKey,
116+
callback: @escaping (Result<Model.Collection, Error>) -> Void) {
117+
do {
118+
// This fails if collection is not signed (i.e., no "signature")
119+
let signedCollection = try self.decoder.decode(JSONModel.SignedCollection.self, from: data)
120+
// Check the signature
121+
self.signatureValidator.validate(signedCollection: signedCollection, certPolicyKey: certPolicyKey) { result in
122+
switch result {
123+
case .failure(let error):
124+
self.diagnosticsEngine?.emit(warning: "The signature of package collection [\(source)] is invalid: \(error)")
125+
callback(.failure(Errors.invalidSignature))
126+
case .success:
127+
callback(self.makeCollection(from: signedCollection.collection, source: source, signature: Model.SignatureData(from: signedCollection.signature)))
128+
}
129+
}
130+
} catch {
131+
// Collection is not signed
132+
guard let collection = try? self.decoder.decode(JSONModel.Collection.self, from: data) else {
133+
return callback(.failure(Errors.invalidJSON(error)))
134+
}
135+
callback(self.makeCollection(from: collection, source: source, signature: nil))
136+
}
137+
}
138+
127139
private func makeCollection(from collection: JSONModel.Collection, source: Model.CollectionSource, signature: Model.SignatureData?) -> Result<Model.Collection, Error> {
128140
if let errors = self.validator.validate(collection: collection)?.errors() {
129141
return .failure(MultipleErrors(errors))
@@ -240,7 +252,10 @@ struct JSONPackageCollectionProvider: PackageCollectionProvider {
240252

241253
public struct Configuration {
242254
public var maximumSizeInBytes: Int64
243-
public var validator: PackageCollectionModel.V1.Validator.Configuration
255+
public var trustedRootCertsDir: URL?
256+
public var sourceCertPolicies: [String: CertificatePolicyKey]
257+
258+
var validator: PackageCollectionModel.V1.Validator.Configuration
244259

245260
public var maximumPackageCount: Int {
246261
get {
@@ -270,22 +285,33 @@ struct JSONPackageCollectionProvider: PackageCollectionProvider {
270285
}
271286

272287
public init(maximumSizeInBytes: Int64? = nil,
288+
trustedRootCertsDir: URL? = nil,
289+
sourceCertPolicies: [String: CertificatePolicyKey]? = nil,
273290
maximumPackageCount: Int? = nil,
274291
maximumMajorVersionCount: Int? = nil,
275292
maximumMinorVersionCount: Int? = nil) {
276293
// TODO: where should we read defaults from?
277294
self.maximumSizeInBytes = maximumSizeInBytes ?? 5_000_000 // 5MB
295+
self.trustedRootCertsDir = trustedRootCertsDir
296+
self.sourceCertPolicies = sourceCertPolicies ?? [:]
278297
self.validator = JSONModel.Validator.Configuration(
279298
maximumPackageCount: maximumPackageCount,
280299
maximumMajorVersionCount: maximumMajorVersionCount,
281300
maximumMinorVersionCount: maximumMinorVersionCount
282301
)
283302
}
303+
304+
func certificatePolicyKey(for source: Model.CollectionSource) -> CertificatePolicyKey? {
305+
// Certificate policy is associated to a collection host
306+
guard let host = source.url.host else { return nil }
307+
return self.sourceCertPolicies[host]
308+
}
284309
}
285310

286311
public enum Errors: Error {
287312
case invalidJSON(Error)
288313
case invalidResponse(String)
314+
case invalidSignature
289315
}
290316
}
291317

Sources/PackageCollectionsModel/PackageCollectionModel+v1.swift

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -473,52 +473,4 @@ extension PackageCollectionModel.V1.SignedCollection: Codable {
473473
self.collection = try PackageCollectionModel.V1.Collection(from: decoder)
474474
self.signature = try container.decode(PackageCollectionModel.V1.Signature.self, forKey: .signature)
475475
}
476-
477-
// MARK: - Extract value for single key path
478-
479-
static let keyPathKey = "key_path"
480-
481-
public static func collection(from data: Data, using decoder: JSONDecoder) throws -> PackageCollectionModel.V1.Collection {
482-
guard let keyPathUserInfoKey = CodingUserInfoKey(rawValue: Self.keyPathKey) else {
483-
throw KeyPathValueError.invalidUserInfo
484-
}
485-
decoder.userInfo[keyPathUserInfoKey] = \PackageCollectionModel.V1.SignedCollection.collection
486-
return try decoder.decode(KeyPathValue<PackageCollectionModel.V1.Collection>.self, from: data).value
487-
}
488-
489-
public static func signature(from data: Data, using decoder: JSONDecoder) throws -> PackageCollectionModel.V1.Signature {
490-
guard let keyPathUserInfoKey = CodingUserInfoKey(rawValue: Self.keyPathKey) else {
491-
throw KeyPathValueError.invalidUserInfo
492-
}
493-
decoder.userInfo[keyPathUserInfoKey] = \PackageCollectionModel.V1.SignedCollection.signature
494-
return try decoder.decode(KeyPathValue<PackageCollectionModel.V1.Signature>.self, from: data).value
495-
}
496-
497-
private struct KeyPathValue<T: Decodable>: Decodable {
498-
let value: T
499-
500-
init(from decoder: Decoder) throws {
501-
guard let keyPathUserInfoKey = CodingUserInfoKey(rawValue: PackageCollectionModel.V1.SignedCollection.keyPathKey) else {
502-
throw KeyPathValueError.invalidUserInfo
503-
}
504-
guard let keyPath = decoder.userInfo[keyPathUserInfoKey] as? KeyPath<PackageCollectionModel.V1.SignedCollection, T> else {
505-
throw KeyPathValueError.missingUserInfo
506-
}
507-
switch keyPath {
508-
case \PackageCollectionModel.V1.SignedCollection.collection:
509-
self.value = try T(from: decoder)
510-
case \PackageCollectionModel.V1.SignedCollection.signature:
511-
let container = try decoder.container(keyedBy: PackageCollectionModel.V1.SignedCollection.CodingKeys.self)
512-
self.value = try container.decode(T.self, forKey: .signature) as T
513-
default:
514-
throw KeyPathValueError.unknownKeyPath
515-
}
516-
}
517-
}
518-
519-
public enum KeyPathValueError: Error {
520-
case invalidUserInfo
521-
case missingUserInfo
522-
case unknownKeyPath
523-
}
524476
}

Sources/PackageCollectionsSigning/PackageCollectionSigning.swift

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,38 @@ import Basics
1515
import PackageCollectionsModel
1616
import TSCBasic
1717

18-
public struct PackageCollectionSigning {
18+
public protocol PackageCollectionSigner {
19+
/// Signs package collection using the given certificate and key.
20+
///
21+
/// - Parameters:
22+
/// - collection: The package collection to be signed
23+
/// - certChainPaths: Paths to all DER-encoded certificates in the chain. The certificate used for signing
24+
/// must be the first in the array.
25+
/// - certPrivateKeyPath: Path to the private key (*.pem) of the certificate
26+
/// - certPolicyKey: The key of the `CertificatePolicy` to use for validating certificates
27+
/// - callback: The callback to invoke when the signed collection is available.
28+
func sign(collection: PackageCollectionModel.V1.Collection,
29+
certChainPaths: [URL],
30+
certPrivateKeyPath: URL,
31+
certPolicyKey: CertificatePolicyKey,
32+
callback: @escaping (Result<PackageCollectionModel.V1.SignedCollection, Error>) -> Void)
33+
}
34+
35+
public protocol PackageCollectionSignatureValidator {
36+
/// Validates a signed package collection.
37+
///
38+
/// - Parameters:
39+
/// - signedCollection: The signed package collection
40+
/// - certPolicyKey: The key of the `CertificatePolicy` to use for validating certificates
41+
/// - callback: The callback to invoke when the result is available.
42+
func validate(signedCollection: PackageCollectionModel.V1.SignedCollection,
43+
certPolicyKey: CertificatePolicyKey,
44+
callback: @escaping (Result<Void, Error>) -> Void)
45+
}
46+
47+
// MARK: - Implementation
48+
49+
public struct PackageCollectionSigning: PackageCollectionSigner, PackageCollectionSignatureValidator {
1950
public typealias Model = PackageCollectionModel.V1
2051

2152
private static let minimumRSAKeySizeInBits = 2048
@@ -31,6 +62,9 @@ public struct PackageCollectionSigning {
3162
/// Internal cache/storage of `CertificatePolicy`s
3263
private let certPolicies = ThreadSafeKeyValueStore<CertificatePolicyKey, CertificatePolicy>()
3364

65+
private let encoder: JSONEncoder
66+
private let decoder: JSONDecoder
67+
3468
public init(trustedRootCertsDir: URL? = nil, callbackQueue: DispatchQueue = DispatchQueue.global(), diagnosticsEngine: DiagnosticsEngine? = nil) {
3569
self.trustedRootCertsDir = trustedRootCertsDir
3670
self.callbackQueue = callbackQueue
@@ -41,13 +75,17 @@ public struct PackageCollectionSigning {
4175
callbackQueue: callbackQueue,
4276
diagnosticsEngine: diagnosticsEngine
4377
)
78+
self.encoder = JSONEncoder.makeWithDefaults()
79+
self.decoder = JSONDecoder.makeWithDefaults()
4480
}
4581

4682
init(certPolicy: CertificatePolicy, trustedRootCertsDir: URL? = nil, callbackQueue: DispatchQueue = DispatchQueue.global(), diagnosticsEngine: DiagnosticsEngine? = nil) {
4783
self.trustedRootCertsDir = trustedRootCertsDir
4884
self.callbackQueue = callbackQueue
49-
self.certPolicies[CertificatePolicyKey.custom] = certPolicy
5085
self.diagnosticsEngine = diagnosticsEngine
86+
self.certPolicies[CertificatePolicyKey.custom] = certPolicy
87+
self.encoder = JSONEncoder.makeWithDefaults()
88+
self.decoder = JSONDecoder.makeWithDefaults()
5189
}
5290

5391
private func getCertificatePolicy(key: CertificatePolicyKey) throws -> CertificatePolicy {
@@ -69,21 +107,10 @@ public struct PackageCollectionSigning {
69107
}
70108
}
71109

72-
/// Signs package collection using the given certificate and key.
73-
///
74-
/// - Parameters:
75-
/// - collection: The package collection to be signed
76-
/// - certChainPaths: Paths to all DER-encoded certificates in the chain. The certificate used for signing
77-
/// must be the first in the array.
78-
/// - certPrivateKeyPath: Path to the private key (*.pem) of the certificate
79-
/// - certPolicyKey: The key of the `CertificatePolicy` to use for validating certificates
80-
/// - jsonEncoder: The `JSONEncoder` to use
81-
/// - callback: The callback to invoke when the signed collection is available.
82110
public func sign(collection: Model.Collection,
83111
certChainPaths: [URL],
84112
certPrivateKeyPath: URL,
85113
certPolicyKey: CertificatePolicyKey = .default,
86-
jsonEncoder: JSONEncoder = JSONEncoder(),
87114
callback: @escaping (Result<Model.SignedCollection, Error>) -> Void) {
88115
do {
89116
let certChainData = try certChainPaths.map { try Data(contentsOf: $0) }
@@ -117,7 +144,7 @@ public struct PackageCollectionSigning {
117144
try self.validateKey(privateKey)
118145

119146
// Generate the signature
120-
let signatureData = try Signature.generate(for: collection, with: header, using: privateKey, jsonEncoder: jsonEncoder)
147+
let signatureData = try Signature.generate(for: collection, with: header, using: privateKey, jsonEncoder: self.encoder)
121148

122149
guard let signature = String(bytes: signatureData, encoding: .utf8) else {
123150
return callback(.failure(PackageCollectionSigningError.invalidSignature))
@@ -150,7 +177,6 @@ public struct PackageCollectionSigning {
150177
/// - callback: The callback to invoke when the result is available.
151178
public func validate(signedCollection: Model.SignedCollection,
152179
certPolicyKey: CertificatePolicyKey = .default,
153-
jsonDecoder: JSONDecoder = JSONDecoder(),
154180
callback: @escaping (Result<Void, Error>) -> Void) {
155181
guard let signature = signedCollection.signature.signature.data(using: .utf8)?.copyBytes() else {
156182
return callback(.failure(PackageCollectionSigningError.invalidSignature))
@@ -160,14 +186,14 @@ public struct PackageCollectionSigning {
160186
let certChainValidate = { certChainData, validateCallback in
161187
self.validateCertChain(certChainData, certPolicyKey: certPolicyKey, callback: validateCallback)
162188
}
163-
Signature.parse(signature, certChainValidate: certChainValidate, jsonDecoder: jsonDecoder) { result in
189+
Signature.parse(signature, certChainValidate: certChainValidate, jsonDecoder: self.decoder) { result in
164190
switch result {
165191
case .failure(let error):
166192
callback(.failure(error))
167193
case .success(let signature):
168194
// Verify the collection embedded in the signature is the same as received
169195
// i.e., the signature is associated with the given collection and not another
170-
guard let collectionFromSignature = try? jsonDecoder.decode(Model.Collection.self, from: signature.payload),
196+
guard let collectionFromSignature = try? self.decoder.decode(Model.Collection.self, from: signature.payload),
171197
signedCollection.collection == collectionFromSignature else {
172198
return callback(.failure(PackageCollectionSigningError.invalidSignature))
173199
}
@@ -208,7 +234,7 @@ public struct PackageCollectionSigning {
208234
}
209235
}
210236

211-
enum PackageCollectionSigningError: Error, Equatable {
237+
public enum PackageCollectionSigningError: Error, Equatable {
212238
case certPolicyNotFound
213239
case emptyCertChain
214240
case invalidCertChain

0 commit comments

Comments
 (0)