Skip to content

[Collections] Signing (apple, 4): signature validation #3271

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Feb 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ let package = Package(
.target(
/** Data structures and support for package collections */
name: "PackageCollections",
dependencies: ["SwiftToolsSupport-auto", "Basics", "PackageModel", "SourceControl", "PackageCollectionsModel"]),
dependencies: ["SwiftToolsSupport-auto", "Basics", "PackageModel", "SourceControl", "PackageCollectionsModel", "PackageCollectionsSigning"]),

// MARK: Package Manager Functionality

Expand Down
7 changes: 6 additions & 1 deletion Sources/Commands/SwiftPackageCollectionsTool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ private enum CollectionsError: Swift.Error {
case invalidVersionString(String)
case unsigned
case cannotVerifySignature
case invalidSignature
}

// FIXME: add links to docs in error messages
Expand All @@ -34,7 +35,9 @@ extension CollectionsError: CustomStringConvertible {
case .unsigned:
return "The collection is not signed. If you would still like to add it please rerun 'add' with '--trust-unsigned'."
case .cannotVerifySignature:
return "The collection's signature cannot be verified due to missing configuration. Please refer to documentations on how to set up trusted root certificates or rerun 'add' with '--skip-signature-check."
return "The collection's signature cannot be verified due to missing configuration. Please refer to documentations on how to set up trusted root certificates or rerun 'add' with '--skip-signature-check'."
case .invalidSignature:
return "The collection's signature is invalid. If you would still like to add it please rerun 'add' with '--skip-signature-check'."
}
}
}
Expand Down Expand Up @@ -134,6 +137,8 @@ public struct SwiftPackageCollectionsTool: ParsableCommand {
throw CollectionsError.unsigned
} catch PackageCollectionError.cannotVerifySignature {
throw CollectionsError.cannotVerifySignature
} catch PackageCollectionError.invalidSignature {
throw CollectionsError.invalidSignature
}
}

Expand Down
2 changes: 2 additions & 0 deletions Sources/PackageCollections/API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,6 @@ public enum PackageCollectionError: Equatable, Error {
/// There are no trusted root certificates. Signature check cannot be done in this case since it involves validating
/// the certificate chain that is used for signing and one requirement is that the root certificate must be trusted.
case cannotVerifySignature

case invalidSignature
}
4 changes: 4 additions & 0 deletions Sources/PackageCollections/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ add_library(PackageCollections
Storage/Trie.swift
API.swift
PackageCollections.swift
PackageCollections+CertificatePolicy.swift
PackageCollections+Configuration.swift
PackageCollections+Storage.swift
PackageCollections+Validation.swift
Expand All @@ -32,6 +33,9 @@ target_link_libraries(PackageCollections PUBLIC
TSCBasic
TSCUtility
Basics
Crypto
PackageCollectionsModel
PackageCollectionsSigning
PackageModel
SourceControl)
# NOTE(compnerd) workaround for CMake not setting up include flags yet
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2021 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See http://swift.org/LICENSE.txt for license information
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import struct Foundation.URL

import PackageCollectionsSigning
import TSCBasic

/// Configuration in this file is intended for package collection sources to define certificate policies
/// that are more restrictive. For example, a source may want to require that all their package
/// collections be signed using certificate that belongs to certain subject user ID.
internal struct PackageCollectionSourceCertificatePolicy {
static let sourceCertPolicies: [String: CertificatePolicyConfig] = [:]

static func certificatePolicyKey(for source: Model.CollectionSource) -> CertificatePolicyKey? {
// Certificate policy is associated to a collection host
source.url.host.flatMap { self.sourceCertPolicies[$0]?.certPolicyKey }
}

static var allRootCerts: [String]? {
let allRootCerts = Self.sourceCertPolicies.values
.compactMap { $0.base64EncodedRootCerts }
.flatMap { $0 }
return allRootCerts.isEmpty ? nil : allRootCerts
}

struct CertificatePolicyConfig {
let certPolicyKey: CertificatePolicyKey

/// Root CAs of the signing certificates. Each item is the base64-encoded string
/// of the DER representation of a root CA.
let base64EncodedRootCerts: [String]?
}
}
2 changes: 1 addition & 1 deletion Sources/PackageCollections/PackageCollections.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public struct PackageCollections: PackageCollectionsProtocol {
}

// initialize with defaults
public init(configuration: Configuration = .init(), diagnosticsEngine: DiagnosticsEngine? = nil) {
public init(configuration: Configuration = .init(), diagnosticsEngine: DiagnosticsEngine = DiagnosticsEngine()) {
let storage = Storage(sources: FilePackageCollectionsSourcesStorage(diagnosticsEngine: diagnosticsEngine),
collections: SQLitePackageCollectionsStorage(diagnosticsEngine: diagnosticsEngine))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,44 @@ import Dispatch
import struct Foundation.Data
import struct Foundation.Date
import class Foundation.JSONDecoder
import class Foundation.ProcessInfo
import struct Foundation.URL

import PackageCollectionsModel
import PackageCollectionsSigning
import PackageModel
import SourceControl
import TSCBasic

private typealias JSONModel = PackageCollectionModel.V1

struct JSONPackageCollectionProvider: PackageCollectionProvider {
// FIXME: remove
static let enableSignatureCheck = ProcessInfo.processInfo.environment["ENABLE_COLLECTION_SIGNATURE_CHECK"] != nil

private let configuration: Configuration
private let diagnosticsEngine: DiagnosticsEngine?
private let diagnosticsEngine: DiagnosticsEngine
private let httpClient: HTTPClient
private let decoder: JSONDecoder
private let validator: JSONModel.Validator
private let signatureValidator: PackageCollectionSignatureValidator

init(configuration: Configuration = .init(), httpClient: HTTPClient? = nil, diagnosticsEngine: DiagnosticsEngine? = nil) {
init(configuration: Configuration = .init(),
httpClient: HTTPClient? = nil,
signatureValidator: PackageCollectionSignatureValidator? = nil,
fileSystem: FileSystem = localFileSystem,
diagnosticsEngine: DiagnosticsEngine) {
self.configuration = configuration
self.diagnosticsEngine = diagnosticsEngine
self.httpClient = httpClient ?? Self.makeDefaultHTTPClient(diagnosticsEngine: diagnosticsEngine)
self.decoder = JSONDecoder.makeWithDefaults()
self.validator = JSONModel.Validator(configuration: configuration.validator)
self.signatureValidator = signatureValidator ?? PackageCollectionSigning(
trustedRootCertsDir: configuration.trustedRootCertsDir ?? fileSystem.dotSwiftPM.appending(components: "config", "trust-root-certs").asURL,
additionalTrustedRootCerts: PackageCollectionSourceCertificatePolicy.allRootCerts,
callbackQueue: DispatchQueue.global(),
diagnosticsEngine: diagnosticsEngine
)
}

func get(_ source: Model.CollectionSource, callback: @escaping (Result<Model.Collection, Error>) -> Void) {
Expand All @@ -51,7 +67,7 @@ struct JSONPackageCollectionProvider: PackageCollectionProvider {
do {
let fileContents = try localFileSystem.readFileContents(absolutePath)
return fileContents.withData { data in
self.decodeAndRunSignatureCheck(source: source, data: data, callback: callback)
self.decodeAndRunSignatureCheck(source: source, data: data, certPolicyKey: .default, callback: callback)
}
} catch {
return callback(.failure(error))
Expand Down Expand Up @@ -93,7 +109,8 @@ struct JSONPackageCollectionProvider: PackageCollectionProvider {
return callback(.failure(Errors.invalidResponse("Body is empty")))
}

self.decodeAndRunSignatureCheck(source: source, data: body, callback: callback)
let certPolicyKey = PackageCollectionSourceCertificatePolicy.certificatePolicyKey(for: source) ?? .default
self.decodeAndRunSignatureCheck(source: source, data: body, certPolicyKey: certPolicyKey, callback: callback)
}
}
}
Expand All @@ -102,24 +119,34 @@ struct JSONPackageCollectionProvider: PackageCollectionProvider {

private func decodeAndRunSignatureCheck(source: Model.CollectionSource,
data: Data,
certPolicyKey: CertificatePolicyKey,
callback: @escaping (Result<Model.Collection, Error>) -> Void) {
do {
// This fails if "signature" is missing
let signature = try JSONModel.SignedCollection.signature(from: data, using: self.decoder)
// This fails if collection is not signed (i.e., no "signature")
let signedCollection = try self.decoder.decode(JSONModel.SignedCollection.self, from: data)

if !Self.enableSignatureCheck {
return callback(self.makeCollection(from: signedCollection.collection, source: source, signature: Model.SignatureData(from: signedCollection.signature, isVerified: false)))
}

if source.skipSignatureCheck {
// Don't validate signature but set isVerified=false
let collection = try JSONModel.SignedCollection.collection(from: data, using: self.decoder)
callback(self.makeCollection(from: collection, source: source, signature: Model.SignatureData(from: signature, isVerified: false)))
// Don't validate signature; set isVerified=false
callback(self.makeCollection(from: signedCollection.collection, source: source, signature: Model.SignatureData(from: signedCollection.signature, isVerified: false)))
} else {
// TODO: Signature validator should throw "cannot verify" error on non-Apple platforms
// if there are no trusted root certs set up, in which case we should throw PackageCollectionError.cannotVerifySignature

// TODO: Check collection's signature
// If signature is
// a. valid: process the collection; set isSigned=true
// b. invalid: includes expired cert, untrusted cert, signature-payload mismatch => return error
let collection = try JSONModel.SignedCollection.collection(from: data, using: self.decoder)
callback(self.makeCollection(from: collection, source: source, signature: Model.SignatureData(from: signature, isVerified: true)))
// Check the signature
self.signatureValidator.validate(signedCollection: signedCollection, certPolicyKey: certPolicyKey) { result in
switch result {
case .failure(let error):
self.diagnosticsEngine.emit(warning: "The signature of package collection [\(source)] is invalid: \(error)")
if PackageCollectionSigningError.noTrustedRootCertsConfigured == error as? PackageCollectionSigningError {
callback(.failure(PackageCollectionError.cannotVerifySignature))
} else {
callback(.failure(PackageCollectionError.invalidSignature))
}
case .success:
callback(self.makeCollection(from: signedCollection.collection, source: source, signature: Model.SignatureData(from: signedCollection.signature, isVerified: true)))
}
}
}
} catch {
// Collection is not signed
Expand Down Expand Up @@ -207,7 +234,7 @@ struct JSONPackageCollectionProvider: PackageCollectionProvider {
}

if !serializationOkay {
self.diagnosticsEngine?.emit(warning: "Some of the information from \(collection.name) could not be deserialized correctly, likely due to invalid format. Contact the collection's author (\(collection.generatedBy?.name ?? "n/a")) to address this issue.")
self.diagnosticsEngine.emit(warning: "Some of the information from \(collection.name) could not be deserialized correctly, likely due to invalid format. Contact the collection's author (\(collection.generatedBy?.name ?? "n/a")) to address this issue.")
}

return .success(.init(source: source,
Expand Down Expand Up @@ -246,7 +273,9 @@ struct JSONPackageCollectionProvider: PackageCollectionProvider {

public struct Configuration {
public var maximumSizeInBytes: Int64
public var validator: PackageCollectionModel.V1.Validator.Configuration
public var trustedRootCertsDir: URL?

var validator: PackageCollectionModel.V1.Validator.Configuration

public var maximumPackageCount: Int {
get {
Expand Down Expand Up @@ -276,11 +305,13 @@ struct JSONPackageCollectionProvider: PackageCollectionProvider {
}

public init(maximumSizeInBytes: Int64? = nil,
trustedRootCertsDir: URL? = nil,
maximumPackageCount: Int? = nil,
maximumMajorVersionCount: Int? = nil,
maximumMinorVersionCount: Int? = nil) {
// TODO: where should we read defaults from?
self.maximumSizeInBytes = maximumSizeInBytes ?? 5_000_000 // 5MB
self.trustedRootCertsDir = trustedRootCertsDir
self.validator = JSONModel.Validator.Configuration(
maximumPackageCount: maximumPackageCount,
maximumMajorVersionCount: maximumMajorVersionCount,
Expand Down
48 changes: 0 additions & 48 deletions Sources/PackageCollectionsModel/PackageCollectionModel+v1.swift
Original file line number Diff line number Diff line change
Expand Up @@ -473,52 +473,4 @@ extension PackageCollectionModel.V1.SignedCollection: Codable {
self.collection = try PackageCollectionModel.V1.Collection(from: decoder)
self.signature = try container.decode(PackageCollectionModel.V1.Signature.self, forKey: .signature)
}

// MARK: - Extract value for single key path

static let keyPathKey = "key_path"

public static func collection(from data: Data, using decoder: JSONDecoder) throws -> PackageCollectionModel.V1.Collection {
guard let keyPathUserInfoKey = CodingUserInfoKey(rawValue: Self.keyPathKey) else {
throw KeyPathValueError.invalidUserInfo
}
decoder.userInfo[keyPathUserInfoKey] = \PackageCollectionModel.V1.SignedCollection.collection
return try decoder.decode(KeyPathValue<PackageCollectionModel.V1.Collection>.self, from: data).value
}

public static func signature(from data: Data, using decoder: JSONDecoder) throws -> PackageCollectionModel.V1.Signature {
guard let keyPathUserInfoKey = CodingUserInfoKey(rawValue: Self.keyPathKey) else {
throw KeyPathValueError.invalidUserInfo
}
decoder.userInfo[keyPathUserInfoKey] = \PackageCollectionModel.V1.SignedCollection.signature
return try decoder.decode(KeyPathValue<PackageCollectionModel.V1.Signature>.self, from: data).value
}

private struct KeyPathValue<T: Decodable>: Decodable {
let value: T

init(from decoder: Decoder) throws {
guard let keyPathUserInfoKey = CodingUserInfoKey(rawValue: PackageCollectionModel.V1.SignedCollection.keyPathKey) else {
throw KeyPathValueError.invalidUserInfo
}
guard let keyPath = decoder.userInfo[keyPathUserInfoKey] as? KeyPath<PackageCollectionModel.V1.SignedCollection, T> else {
throw KeyPathValueError.missingUserInfo
}
switch keyPath {
case \PackageCollectionModel.V1.SignedCollection.collection:
self.value = try T(from: decoder)
case \PackageCollectionModel.V1.SignedCollection.signature:
let container = try decoder.container(keyedBy: PackageCollectionModel.V1.SignedCollection.CodingKeys.self)
self.value = try container.decode(T.self, forKey: .signature) as T
default:
throw KeyPathValueError.unknownKeyPath
}
}
}

public enum KeyPathValueError: Error {
case invalidUserInfo
case missingUserInfo
case unknownKeyPath
}
}
Loading