From cf628908fe04eae8e71b0a6d93ae13bda3fbfe13 Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Thu, 23 Feb 2023 21:01:27 -0800 Subject: [PATCH 01/20] Update registry config to match proposal --- Sources/PackageRegistry/RegistryConfiguration.swift | 2 +- Tests/PackageRegistryTests/RegistryConfigurationTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/PackageRegistry/RegistryConfiguration.swift b/Sources/PackageRegistry/RegistryConfiguration.swift index 84590f6f0ba..36e7202a991 100644 --- a/Sources/PackageRegistry/RegistryConfiguration.swift +++ b/Sources/PackageRegistry/RegistryConfiguration.swift @@ -210,7 +210,7 @@ extension RegistryConfiguration { case error case prompt case warn - case silentTrust + case silentAllow } public struct ValidationChecks: Hashable, Codable { diff --git a/Tests/PackageRegistryTests/RegistryConfigurationTests.swift b/Tests/PackageRegistryTests/RegistryConfigurationTests.swift index 738f1a776a5..1b9f0307c47 100644 --- a/Tests/PackageRegistryTests/RegistryConfigurationTests.swift +++ b/Tests/PackageRegistryTests/RegistryConfigurationTests.swift @@ -65,7 +65,7 @@ final class RegistryConfigurationTests: XCTestCase { var registryOverride = RegistryConfiguration.Security.RegistryOverride() registryOverride.signing = RegistryConfiguration.Security.Signing() registryOverride.signing?.onUnsigned = .silentAllow - registryOverride.signing?.onUntrustedCertificate = .silentTrust + registryOverride.signing?.onUntrustedCertificate = .silentAllow registryOverride.signing?.trustedRootCertificatesPath = "/foo/roots" registryOverride.signing?.includeDefaultTrustedRootCertificates = false registryOverride.signing?.validationChecks = RegistryConfiguration.Security.Signing.ValidationChecks() From 81ee8794862b5d40dff8c6d4bec1eeec755cd22a Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Thu, 23 Feb 2023 21:34:42 -0800 Subject: [PATCH 02/20] Setup CMake build for PackageSigning. Add dependency to PackageRegistry. --- Package.swift | 8 +++++--- Sources/CMakeLists.txt | 1 + Sources/PackageRegistry/CMakeLists.txt | 1 + Sources/PackageSigning/CMakeLists.txt | 20 ++++++++++++++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 Sources/PackageSigning/CMakeLists.txt diff --git a/Package.swift b/Package.swift index 27c083d9277..5baff990d09 100644 --- a/Package.swift +++ b/Package.swift @@ -4,7 +4,7 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2014-2022 Apple Inc. and the Swift project authors +// Copyright (c) 2014-2023 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 @@ -204,7 +204,8 @@ let package = Package( "Basics", "PackageFingerprint", "PackageLoading", - "PackageModel" + "PackageModel", + "PackageSigning", ], exclude: ["CMakeLists.txt"] ), @@ -296,7 +297,8 @@ let package = Package( .product(name: "Crypto", package: "swift-crypto"), "Basics", "PackageModel", - ] + ], + exclude: ["CMakeLists.txt"] ), // MARK: Package Manager Functionality diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index 7ba725afc2e..f79aa7b3114 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -22,6 +22,7 @@ add_subdirectory(PackageLoading) add_subdirectory(PackageModel) add_subdirectory(PackagePlugin) add_subdirectory(PackageRegistry) +add_subdirectory(PackageSigning) add_subdirectory(SPMBuildCore) add_subdirectory(SPMLLBuild) add_subdirectory(SourceControl) diff --git a/Sources/PackageRegistry/CMakeLists.txt b/Sources/PackageRegistry/CMakeLists.txt index 80dcb514c7d..d2bbe9b8729 100644 --- a/Sources/PackageRegistry/CMakeLists.txt +++ b/Sources/PackageRegistry/CMakeLists.txt @@ -17,6 +17,7 @@ target_link_libraries(PackageRegistry PUBLIC PackageFingerprint PackageLoading PackageModel + PackageSigning TSCBasic TSCUtility) # NOTE(compnerd) workaround for CMake not setting up include flags yet diff --git a/Sources/PackageSigning/CMakeLists.txt b/Sources/PackageSigning/CMakeLists.txt new file mode 100644 index 00000000000..8569f3fcd7b --- /dev/null +++ b/Sources/PackageSigning/CMakeLists.txt @@ -0,0 +1,20 @@ +# This source file is part of the Swift open source project +# +# Copyright (c) 2023 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 + +add_library(PackageSigning STATIC + SignatureProvider.swift + SigningEntity.swift + SigningIdentity.swift) +target_link_libraries(PackageSigning PUBLIC + $<$>:Foundation> + Basics) +# NOTE(compnerd) workaround for CMake not setting up include flags yet +set_target_properties(PackageSigning PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) + +set_property(GLOBAL APPEND PROPERTY SwiftPM_EXPORTS PackageSigning) From 96c1f49c3a8266da062d8b6e9ba7c60652d04a0a Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Fri, 24 Feb 2023 00:16:35 -0800 Subject: [PATCH 03/20] API tweaks --- .../PackageSigning/SignatureProvider.swift | 40 +++++-------------- .../SigningEntity/SigningEntity.swift | 5 --- Tests/PackageSigningTests/SigningTests.swift | 5 ++- 3 files changed, 13 insertions(+), 37 deletions(-) diff --git a/Sources/PackageSigning/SignatureProvider.swift b/Sources/PackageSigning/SignatureProvider.swift index 00966dceb52..c6dcdf3b17c 100644 --- a/Sources/PackageSigning/SignatureProvider.swift +++ b/Sources/PackageSigning/SignatureProvider.swift @@ -38,7 +38,12 @@ public enum SignatureProvider { observabilityScope: ObservabilityScope ) async throws -> SignatureStatus { let provider = format.provider - return try await provider.status(of: signature, for: content, observabilityScope: observabilityScope) + return try await provider.status( + of: signature, + for: content, + verifierConfiguration: verifierConfiguration, + observabilityScope: observabilityScope + ) } } @@ -66,7 +71,7 @@ public struct VerifierConfiguration { } public enum SignatureStatus: Equatable { - case valid + case valid(SigningEntity) case invalid(String) case certificateInvalid(String) case certificateNotTrusted @@ -97,10 +102,9 @@ protocol SignatureProviderProtocol { func status( of signature: Data, for content: Data, + verifierConfiguration: VerifierConfiguration, observabilityScope: ObservabilityScope ) async throws -> SignatureStatus - - func signingEntity(of signature: Data) throws -> SigningEntity } public enum SignatureFormat: String { @@ -185,6 +189,7 @@ struct CMSSignatureProvider: SignatureProviderProtocol { func status( of signature: Data, for content: Data, + verifierConfiguration: VerifierConfiguration, observabilityScope: ObservabilityScope ) async throws -> SignatureStatus { #if os(macOS) @@ -250,39 +255,14 @@ struct CMSSignatureProvider: SignatureProviderProtocol { } observabilityScope.emit(debug: "Certificate revocation status: \(String(describing: revocationStatus))") - return .valid - #else - fatalError("TO BE IMPLEMENTED") - #endif - } - - func signingEntity(of signature: Data) throws -> SigningEntity { - #if os(macOS) - var cmsDecoder: CMSDecoder? - var status = CMSDecoderCreate(&cmsDecoder) - guard status == errSecSuccess, let cmsDecoder = cmsDecoder else { - throw SigningError.decodeInitializationFailed("Unable to create CMSDecoder. Error: \(status)") - } - - status = CMSDecoderUpdateMessage(cmsDecoder, [UInt8](signature), signature.count) - guard status == errSecSuccess else { - throw SigningError - .decodeInitializationFailed("Unable to update CMSDecoder with signature. Error: \(status)") - } - status = CMSDecoderFinalizeMessage(cmsDecoder) - guard status == errSecSuccess else { - throw SigningError.decodeInitializationFailed("Failed to set up CMSDecoder. Error: \(status)") - } - var certificate: SecCertificate? status = CMSDecoderCopySignerCert(cmsDecoder, 0, &certificate) guard status == errSecSuccess, let certificate = certificate else { throw SigningError.signatureInvalid("Unable to extract signing certificate. Error: \(status)") } - return SigningEntity(certificate: certificate) + return .valid(SigningEntity(certificate: certificate)) #else - // TODO: decode `data` by `format`, then construct `signedBy` from signing cert fatalError("TO BE IMPLEMENTED") #endif } diff --git a/Sources/PackageSigning/SigningEntity/SigningEntity.swift b/Sources/PackageSigning/SigningEntity/SigningEntity.swift index 6c70ccef61f..daef0b26a25 100644 --- a/Sources/PackageSigning/SigningEntity/SigningEntity.swift +++ b/Sources/PackageSigning/SigningEntity/SigningEntity.swift @@ -40,11 +40,6 @@ public struct SigningEntity: Hashable, Codable, CustomStringConvertible { self.organization = organization } - public init(of signature: Data, signatureFormat: SignatureFormat) throws { - let provider = signatureFormat.provider - self = try provider.signingEntity(of: signature) - } - #if os(macOS) init(certificate: SecCertificate) { self.type = certificate.signingEntityType diff --git a/Tests/PackageSigningTests/SigningTests.swift b/Tests/PackageSigningTests/SigningTests.swift index f989d374087..3adc98213bf 100644 --- a/Tests/PackageSigningTests/SigningTests.swift +++ b/Tests/PackageSigningTests/SigningTests.swift @@ -58,9 +58,10 @@ final class SigningTests: XCTestCase { verifierConfiguration: .init(), observabilityScope: ObservabilitySystem.NOOP ) - XCTAssertEqual(status, SignatureStatus.valid) - let signingEntity = try SigningEntity(of: signature, signatureFormat: signatureFormat) + guard case .valid(let signingEntity) = status else { + return XCTFail("Expected signature status to be .valid but got \(status)") + } XCTAssertNotNil(signingEntity.name) XCTAssertNotNil(signingEntity.organizationalUnit) XCTAssertNotNil(signingEntity.organization) From b23260bb582f37b00b6e5e2e80ffd42f5c6b792c Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Fri, 24 Feb 2023 00:28:42 -0800 Subject: [PATCH 04/20] Add signature model to package version metadata --- Sources/PackageRegistry/RegistryClient.swift | 9 ++++++++- Sources/SPMTestSupport/MockRegistry.swift | 3 ++- Tests/WorkspaceTests/RegistryPackageContainerTests.swift | 3 ++- Tests/WorkspaceTests/WorkspaceTests.swift | 3 ++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Sources/PackageRegistry/RegistryClient.swift b/Sources/PackageRegistry/RegistryClient.swift index 064e249472e..75d8b21c34f 100644 --- a/Sources/PackageRegistry/RegistryClient.swift +++ b/Sources/PackageRegistry/RegistryClient.swift @@ -1623,13 +1623,20 @@ extension RegistryClient { public let name: String public let type: String public let checksum: String? + public let signing: Signing? - public init(name: String, type: String, checksum: String) { + public init(name: String, type: String, checksum: String, signing: Signing?) { self.name = name self.type = type self.checksum = checksum + self.signing = signing } } + + public struct Signing: Codable { + public let signatureBase64Encoded: String + public let signatureFormat: String + } public struct AdditionalMetadata: Codable { public let author: Author? diff --git a/Sources/SPMTestSupport/MockRegistry.swift b/Sources/SPMTestSupport/MockRegistry.swift index b12a935050d..2c401976f56 100644 --- a/Sources/SPMTestSupport/MockRegistry.swift +++ b/Sources/SPMTestSupport/MockRegistry.swift @@ -215,7 +215,8 @@ public class MockRegistry { .init( name: "source-archive", type: "application/zip", - checksum: zipfileChecksum.hexadecimalRepresentation + checksum: zipfileChecksum.hexadecimalRepresentation, + signing: nil ), ], metadata: .init( diff --git a/Tests/WorkspaceTests/RegistryPackageContainerTests.swift b/Tests/WorkspaceTests/RegistryPackageContainerTests.swift index 50cfab5432a..8782ec9f97a 100644 --- a/Tests/WorkspaceTests/RegistryPackageContainerTests.swift +++ b/Tests/WorkspaceTests/RegistryPackageContainerTests.swift @@ -371,7 +371,8 @@ class RegistryPackageContainerTests: XCTestCase { .init( name: "source-archive", type: "application/zip", - checksum: "" + checksum: "", + signing: nil ) ], metadata: .init(description: "") diff --git a/Tests/WorkspaceTests/WorkspaceTests.swift b/Tests/WorkspaceTests/WorkspaceTests.swift index c6fa3716d03..e25da682c9a 100644 --- a/Tests/WorkspaceTests/WorkspaceTests.swift +++ b/Tests/WorkspaceTests/WorkspaceTests.swift @@ -12803,7 +12803,8 @@ final class WorkspaceTests: XCTestCase { .init( name: "source-archive", type: "application/zip", - checksum: "" + checksum: "", + signing: nil ) ], metadata: .init(description: "") From 48d089f74363544b3bbd89bbc1dae63fc7feacec Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Fri, 24 Feb 2023 21:24:23 -0800 Subject: [PATCH 05/20] update cmake build --- Sources/PackageSigning/CMakeLists.txt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/PackageSigning/CMakeLists.txt b/Sources/PackageSigning/CMakeLists.txt index 8569f3fcd7b..43a3c2c6dbe 100644 --- a/Sources/PackageSigning/CMakeLists.txt +++ b/Sources/PackageSigning/CMakeLists.txt @@ -7,12 +7,18 @@ # See http://swift.org/CONTRIBUTORS.txt for Swift project authors add_library(PackageSigning STATIC + SigningEntity/FilePackageSigningEntityStorage.swift + SigningEntity/PackageSigningEntityStorage.swift + SigningEntity/SigningEntity.swift SignatureProvider.swift - SigningEntity.swift SigningIdentity.swift) target_link_libraries(PackageSigning PUBLIC + $<$>:dispatch> $<$>:Foundation> - Basics) + Basics + PackageModel + TSCBasic + TSCUtility) # NOTE(compnerd) workaround for CMake not setting up include flags yet set_target_properties(PackageSigning PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) From 4551303b903989bb38010edc1db9d4173297fe94 Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Sat, 25 Feb 2023 00:06:23 -0800 Subject: [PATCH 06/20] Wire up PackageSigningEntityStorage --- Package.swift | 3 +- Sources/CoreCommands/Options.swift | 13 +-- Sources/CoreCommands/SwiftTool.swift | 3 +- Sources/PackageRegistry/RegistryClient.swift | 7 ++ .../PackageRegistryTool+Auth.swift | 2 + .../PackageRegistryTool+Publish.swift | 2 + .../FilePackageSigningEntityStorage.swift | 2 +- .../PackageSigningEntityStorage.swift | 5 ++ .../MockPackageSigningEntityStorage.swift | 87 +++++++++++++++++++ Sources/SPMTestSupport/MockRegistry.swift | 6 +- Sources/SPMTestSupport/MockWorkspace.swift | 9 +- Sources/Workspace/CMakeLists.txt | 3 +- .../Workspace/Workspace+Configuration.swift | 22 ++++- Sources/Workspace/Workspace.swift | 42 ++++++++- .../RegistryClientTests.swift | 23 ++++- .../RegistryDownloadsManagerTests.swift | 9 +- .../RegistryPackageContainerTests.swift | 4 +- Tests/WorkspaceTests/WorkspaceTests.swift | 8 +- 18 files changed, 226 insertions(+), 24 deletions(-) create mode 100644 Sources/SPMTestSupport/MockPackageSigningEntityStorage.swift diff --git a/Package.swift b/Package.swift index 5baff990d09..804e45247b6 100644 --- a/Package.swift +++ b/Package.swift @@ -350,6 +350,7 @@ let package = Package( "PackageGraph", "PackageModel", "PackageRegistry", + "PackageSigning", "SourceControl", "SPMBuildCore", ], @@ -375,7 +376,6 @@ let package = Package( .product(name: "ArgumentParser", package: "swift-argument-parser"), "Basics", "Build", - "PackageFingerprint", "PackageLoading", "PackageModel", "PackageGraph", @@ -542,6 +542,7 @@ let package = Package( "PackageGraph", "PackageLoading", "PackageRegistry", + "PackageSigning", "SourceControl", .product(name: "TSCTestSupport", package: "swift-tools-support-core"), "Workspace", diff --git a/Sources/CoreCommands/Options.swift b/Sources/CoreCommands/Options.swift index d56a77b359d..a38f88c2a2b 100644 --- a/Sources/CoreCommands/Options.swift +++ b/Sources/CoreCommands/Options.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2014-2022 Apple Inc. and the Swift project authors +// Copyright (c) 2014-2023 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 @@ -14,8 +14,6 @@ import ArgumentParser import struct Foundation.URL -import enum PackageFingerprint.FingerprintCheckingMode - import enum PackageModel.BuildConfiguration import struct PackageModel.BuildFlags import struct PackageModel.EnabledSanitizers @@ -31,6 +29,8 @@ import struct TSCBasic.StringError import struct TSCUtility.Triple import struct TSCUtility.Version +import struct Workspace.WorkspaceConfiguration + public struct GlobalOptions: ParsableArguments { public init() {} @@ -217,7 +217,10 @@ public struct SecurityOptions: ParsableArguments { #endif @Option(name: .customLong("resolver-fingerprint-checking")) - public var fingerprintCheckingMode: FingerprintCheckingMode = .strict + public var fingerprintCheckingMode: WorkspaceConfiguration.CheckingMode = .strict + + @Option(name: .customLong("resolver-signing-entity-checking")) + public var signingEntityCheckingMode: WorkspaceConfiguration.CheckingMode = .warn } public struct ResolverOptions: ParsableArguments { @@ -473,7 +476,7 @@ extension AbsolutePath: ExpressibleByArgument { } } -extension FingerprintCheckingMode: ExpressibleByArgument { +extension WorkspaceConfiguration.CheckingMode: ExpressibleByArgument { public init?(argument: String) { self.init(rawValue: argument) } diff --git a/Sources/CoreCommands/SwiftTool.swift b/Sources/CoreCommands/SwiftTool.swift index 26c60a55230..88386167212 100644 --- a/Sources/CoreCommands/SwiftTool.swift +++ b/Sources/CoreCommands/SwiftTool.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2014-2022 Apple Inc. and the Swift project authors +// Copyright (c) 2014-2023 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 @@ -463,6 +463,7 @@ public final class SwiftTool { additionalFileRules: isXcodeBuildSystemEnabled ? FileRuleDescription.xcbuildFileTypes : FileRuleDescription.swiftpmFileTypes, sharedDependenciesCacheEnabled: self.options.caching.useDependenciesCache, fingerprintCheckingMode: self.options.security.fingerprintCheckingMode, + signingEntityCheckingMode: self.options.security.signingEntityCheckingMode, sourceControlToRegistryDependencyTransformation: self.options.resolver.sourceControlToRegistryDependencyTransformation.workspaceConfiguration, restrictImports: .none ), diff --git a/Sources/PackageRegistry/RegistryClient.swift b/Sources/PackageRegistry/RegistryClient.swift index 75d8b21c34f..1bcff5e563d 100644 --- a/Sources/PackageRegistry/RegistryClient.swift +++ b/Sources/PackageRegistry/RegistryClient.swift @@ -16,6 +16,7 @@ import Foundation import PackageFingerprint import PackageLoading import PackageModel +import PackageSigning import TSCBasic import struct TSCUtility.Version @@ -30,6 +31,8 @@ public final class RegistryClient: Cancellable { private let archiverProvider: (FileSystem) -> Archiver private let httpClient: LegacyHTTPClient private let authorizationProvider: LegacyHTTPClientConfiguration.AuthorizationProvider? + private let signingEntityStorage: PackageSigningEntityStorage? + private let signingEntityCheckingMode: SigningEntityCheckingMode private let jsonDecoder: JSONDecoder private var checksumTOFU: PackageVersionChecksumTOFU! @@ -43,6 +46,8 @@ public final class RegistryClient: Cancellable { configuration: RegistryConfiguration, fingerprintStorage: PackageFingerprintStorage?, fingerprintCheckingMode: FingerprintCheckingMode, + signingEntityStorage: PackageSigningEntityStorage?, + signingEntityCheckingMode: SigningEntityCheckingMode, authorizationProvider: AuthorizationProvider? = .none, customHTTPClient: LegacyHTTPClient? = .none, customArchiverProvider: ((FileSystem) -> Archiver)? = .none @@ -75,6 +80,8 @@ public final class RegistryClient: Cancellable { self.httpClient = customHTTPClient ?? LegacyHTTPClient() self.archiverProvider = customArchiverProvider ?? { fileSystem in ZipArchiver(fileSystem: fileSystem) } + self.signingEntityStorage = signingEntityStorage + self.signingEntityCheckingMode = signingEntityCheckingMode self.jsonDecoder = JSONDecoder.makeWithDefaults() self.checksumTOFU = PackageVersionChecksumTOFU( diff --git a/Sources/PackageRegistryTool/PackageRegistryTool+Auth.swift b/Sources/PackageRegistryTool/PackageRegistryTool+Auth.swift index 25aeae35d7e..de2b9b9df6f 100644 --- a/Sources/PackageRegistryTool/PackageRegistryTool+Auth.swift +++ b/Sources/PackageRegistryTool/PackageRegistryTool+Auth.swift @@ -182,6 +182,8 @@ extension SwiftPackageRegistryTool { configuration: registryConfiguration, fingerprintStorage: .none, fingerprintCheckingMode: .strict, + signingEntityStorage: .none, + signingEntityCheckingMode: .strict, authorizationProvider: authorizationProvider ) diff --git a/Sources/PackageRegistryTool/PackageRegistryTool+Publish.swift b/Sources/PackageRegistryTool/PackageRegistryTool+Publish.swift index 939f092dbf4..a68f33f4a08 100644 --- a/Sources/PackageRegistryTool/PackageRegistryTool+Publish.swift +++ b/Sources/PackageRegistryTool/PackageRegistryTool+Publish.swift @@ -127,6 +127,8 @@ extension SwiftPackageRegistryTool { configuration: configuration, fingerprintStorage: .none, fingerprintCheckingMode: .strict, + signingEntityStorage: .none, + signingEntityCheckingMode: .strict, authorizationProvider: authorizationProvider ) diff --git a/Sources/PackageSigning/SigningEntity/FilePackageSigningEntityStorage.swift b/Sources/PackageSigning/SigningEntity/FilePackageSigningEntityStorage.swift index 5a0e1316e0e..f27f2428656 100644 --- a/Sources/PackageSigning/SigningEntity/FilePackageSigningEntityStorage.swift +++ b/Sources/PackageSigning/SigningEntity/FilePackageSigningEntityStorage.swift @@ -163,7 +163,7 @@ extension PackageIdentity { } extension Dictionary where Key == SigningEntity, Value == Set { - fileprivate func signingEntity(of version: Version) -> SigningEntity? { + public func signingEntity(of version: Version) -> SigningEntity? { for (signingEntity, versions) in self { if versions.contains(version) { return signingEntity diff --git a/Sources/PackageSigning/SigningEntity/PackageSigningEntityStorage.swift b/Sources/PackageSigning/SigningEntity/PackageSigningEntityStorage.swift index e9b63041d3a..aa9b4234860 100644 --- a/Sources/PackageSigning/SigningEntity/PackageSigningEntityStorage.swift +++ b/Sources/PackageSigning/SigningEntity/PackageSigningEntityStorage.swift @@ -46,3 +46,8 @@ public enum PackageSigningEntityStorageError: Error, Equatable, CustomStringConv } } } + +public enum SigningEntityCheckingMode: String { + case strict + case warn +} diff --git a/Sources/SPMTestSupport/MockPackageSigningEntityStorage.swift b/Sources/SPMTestSupport/MockPackageSigningEntityStorage.swift new file mode 100644 index 00000000000..98e70c60d0c --- /dev/null +++ b/Sources/SPMTestSupport/MockPackageSigningEntityStorage.swift @@ -0,0 +1,87 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2023 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 the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Basics +import Dispatch +import class Foundation.NSLock +import PackageModel +import PackageSigning + +import struct TSCUtility.Version + +public class MockPackageSigningEntityStorage: PackageSigningEntityStorage { + private var packageSignedVersions: [PackageIdentity: [SigningEntity: Set]] + private let lock = NSLock() + + public init(_ packageSignedVersions: [PackageIdentity: [SigningEntity: Set]] = [:]) { + self.packageSignedVersions = packageSignedVersions + } + + public func get( + package: PackageIdentity, + observabilityScope: ObservabilityScope, + callbackQueue: DispatchQueue, + callback: @escaping (Result<[SigningEntity: Set], Error>) -> Void + ) { + if let signedVersions = self.lock.withLock({ self.packageSignedVersions[package] }) { + callbackQueue.async { + callback(.success(signedVersions)) + } + } else { + callbackQueue.async { + callback(.success([:])) + } + } + } + + public func put( + package: PackageIdentity, + version: Version, + signingEntity: SigningEntity, + observabilityScope: ObservabilityScope, + callbackQueue: DispatchQueue, + callback: @escaping (Result) -> Void + ) { + do { + try self.lock.withLock { + var signedVersions = self.packageSignedVersions[package] ?? [:] + + if let existing = signedVersions.signingEntity(of: version) { + // Error if we try to write a different signing entity for a version + guard signingEntity == existing else { + throw PackageSigningEntityStorageError.conflict( + package: package, + version: version, + given: signingEntity, + existing: existing + ) + } + // Don't need to do anything if signing entities are the same + return + } + + var versions = signedVersions.removeValue(forKey: signingEntity) ?? [] + versions.insert(version) + signedVersions[signingEntity] = versions + self.packageSignedVersions[package] = signedVersions + } + + callbackQueue.async { + callback(.success(())) + } + } catch { + callbackQueue.async { + callback(.failure(error)) + } + } + } +} diff --git a/Sources/SPMTestSupport/MockRegistry.swift b/Sources/SPMTestSupport/MockRegistry.swift index 2c401976f56..18965d25b38 100644 --- a/Sources/SPMTestSupport/MockRegistry.swift +++ b/Sources/SPMTestSupport/MockRegistry.swift @@ -17,6 +17,7 @@ import PackageGraph import PackageLoading import PackageModel import PackageRegistry +import PackageSigning import TSCBasic import struct TSCUtility.Version @@ -39,7 +40,8 @@ public class MockRegistry { filesystem: FileSystem, identityResolver: IdentityResolver, checksumAlgorithm: HashAlgorithm, - fingerprintStorage: PackageFingerprintStorage + fingerprintStorage: PackageFingerprintStorage, + signingEntityStorage: PackageSigningEntityStorage ) { self.fileSystem = filesystem self.identityResolver = identityResolver @@ -53,6 +55,8 @@ public class MockRegistry { configuration: configuration, fingerprintStorage: fingerprintStorage, fingerprintCheckingMode: .strict, + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: .strict, authorizationProvider: .none, customHTTPClient: LegacyHTTPClient(handler: self.httpHandler), customArchiverProvider: { fileSystem in MockRegistryArchiver(fileSystem: fileSystem) } diff --git a/Sources/SPMTestSupport/MockWorkspace.swift b/Sources/SPMTestSupport/MockWorkspace.swift index 284e08b4fd2..b38f3f1cd12 100644 --- a/Sources/SPMTestSupport/MockWorkspace.swift +++ b/Sources/SPMTestSupport/MockWorkspace.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2014-2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014-2023 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 @@ -31,6 +31,7 @@ public final class MockWorkspace { let packages: [MockPackage] let customToolsVersion: ToolsVersion? let fingerprints: MockPackageFingerprintStorage + let signingEntities: MockPackageSigningEntityStorage let mirrors: DependencyMirrors public var registryClient: RegistryClient let registry: MockRegistry @@ -51,6 +52,7 @@ public final class MockWorkspace { packages: [MockPackage] = [], toolsVersion customToolsVersion: ToolsVersion? = .none, fingerprints customFingerprints: MockPackageFingerprintStorage? = .none, + signingEntities customSigningEntities: MockPackageSigningEntityStorage? = .none, mirrors customMirrors: DependencyMirrors? = nil, registryClient customRegistryClient: RegistryClient? = .none, binaryArtifactsManager customBinaryArtifactsManager: Workspace.CustomBinaryArtifactsManager? = .none, @@ -64,6 +66,7 @@ public final class MockWorkspace { self.roots = roots self.packages = packages self.fingerprints = customFingerprints ?? MockPackageFingerprintStorage() + self.signingEntities = customSigningEntities ?? MockPackageSigningEntityStorage() self.mirrors = customMirrors ?? DependencyMirrors() self.identityResolver = DefaultIdentityResolver( locationMapper: self.mirrors.effectiveURL(for:), @@ -77,7 +80,8 @@ public final class MockWorkspace { filesystem: self.fileSystem, identityResolver: self.identityResolver, checksumAlgorithm: self.checksumAlgorithm, - fingerprintStorage: self.fingerprints + fingerprintStorage: self.fingerprints, + signingEntityStorage: self.signingEntities ) self.registryClient = customRegistryClient ?? self.registry.registryClient self.customToolsVersion = customToolsVersion @@ -271,6 +275,7 @@ public final class MockWorkspace { additionalFileRules: WorkspaceConfiguration.default.additionalFileRules, sharedDependenciesCacheEnabled: WorkspaceConfiguration.default.sharedDependenciesCacheEnabled, fingerprintCheckingMode: .strict, + signingEntityCheckingMode: .strict, sourceControlToRegistryDependencyTransformation: self.sourceControlToRegistryDependencyTransformation, restrictImports: .none ), diff --git a/Sources/Workspace/CMakeLists.txt b/Sources/Workspace/CMakeLists.txt index 352ff7fd9de..c8fc0fb430b 100644 --- a/Sources/Workspace/CMakeLists.txt +++ b/Sources/Workspace/CMakeLists.txt @@ -1,6 +1,6 @@ # This source file is part of the Swift open source project # -# Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +# Copyright (c) 2014 - 2023 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 @@ -33,6 +33,7 @@ target_link_libraries(Workspace PUBLIC PackageLoading PackageModel PackageRegistry + PackageSigning SourceControl) target_link_libraries(PackageLoading PUBLIC $<$>:Foundation>) diff --git a/Sources/Workspace/Workspace+Configuration.swift b/Sources/Workspace/Workspace+Configuration.swift index 3b00b19da08..952c57de4e5 100644 --- a/Sources/Workspace/Workspace+Configuration.swift +++ b/Sources/Workspace/Workspace+Configuration.swift @@ -115,6 +115,11 @@ extension Workspace { public var sharedFingerprintsDirectory: AbsolutePath? { self.sharedSecurityDirectory.map { $0.appending(component: "fingerprints") } } + + /// Path to the shared directory where package signing records are kept. + public var sharedSigningEntitiesDirectory: AbsolutePath? { + self.sharedSecurityDirectory.map { $0.appending(component: "signing-entities") } + } /// Path to the shared trusted root certificates directory. public var sharedTrustedRootCertificatesDirectory: AbsolutePath? { @@ -679,8 +684,11 @@ public struct WorkspaceConfiguration { /// Enables the shared dependencies cache. Enabled by default. public var sharedDependenciesCacheEnabled: Bool - /// Fingerprint checking mode. Defaults to warn. - public var fingerprintCheckingMode: FingerprintCheckingMode + /// Fingerprint checking mode. Defaults to strict. + public var fingerprintCheckingMode: CheckingMode + + /// Signing entity checking mode. Defaults to warn. + public var signingEntityCheckingMode: CheckingMode /// Attempt to transform source control based dependencies to registry ones public var sourceControlToRegistryDependencyTransformation: SourceControlToRegistryDependencyTransformation @@ -701,7 +709,8 @@ public struct WorkspaceConfiguration { createREPLProduct: Bool, additionalFileRules: [FileRuleDescription], sharedDependenciesCacheEnabled: Bool, - fingerprintCheckingMode: FingerprintCheckingMode, + fingerprintCheckingMode: CheckingMode, + signingEntityCheckingMode: CheckingMode, sourceControlToRegistryDependencyTransformation: SourceControlToRegistryDependencyTransformation, restrictImports: (startingToolsVersion: ToolsVersion, allowedImports: [String])? ) { @@ -712,6 +721,7 @@ public struct WorkspaceConfiguration { self.additionalFileRules = additionalFileRules self.sharedDependenciesCacheEnabled = sharedDependenciesCacheEnabled self.fingerprintCheckingMode = fingerprintCheckingMode + self.signingEntityCheckingMode = signingEntityCheckingMode self.sourceControlToRegistryDependencyTransformation = sourceControlToRegistryDependencyTransformation self.restrictImports = restrictImports } @@ -726,6 +736,7 @@ public struct WorkspaceConfiguration { additionalFileRules: [], sharedDependenciesCacheEnabled: true, fingerprintCheckingMode: .strict, + signingEntityCheckingMode: .warn, sourceControlToRegistryDependencyTransformation: .disabled, restrictImports: .none ) @@ -736,6 +747,11 @@ public struct WorkspaceConfiguration { case identity case swizzle } + + public enum CheckingMode: String { + case strict + case warn + } } // MARK: - Deprecated 8/20201 diff --git a/Sources/Workspace/Workspace.swift b/Sources/Workspace/Workspace.swift index 2c1406a3cbc..61e7efe78a7 100644 --- a/Sources/Workspace/Workspace.swift +++ b/Sources/Workspace/Workspace.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2014-2022 Apple Inc. and the Swift project authors +// Copyright (c) 2014-2023 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 @@ -19,6 +19,7 @@ import PackageModel import PackageFingerprint import PackageGraph import PackageRegistry +import PackageSigning import SourceControl import TSCBasic @@ -350,6 +351,7 @@ public class Workspace { initializationWarningHandler: initializationWarningHandler, customRegistriesConfiguration: .none, customFingerprints: .none, + customSigningEntities: .none, customMirrors: .none, customToolsVersion: .none, customHostToolchain: customHostToolchain, @@ -486,6 +488,7 @@ public class Workspace { // optional customization, primarily designed for testing but also used in some cases by libSwiftPM consumers customRegistriesConfiguration: RegistryConfiguration? = .none, customFingerprints: PackageFingerprintStorage? = .none, + customSigningEntities: PackageSigningEntityStorage? = .none, customMirrors: DependencyMirrors? = .none, customToolsVersion: ToolsVersion? = .none, customHostToolchain: UserToolchain? = .none, @@ -510,6 +513,7 @@ public class Workspace { initializationWarningHandler: initializationWarningHandler, customRegistriesConfiguration: customRegistriesConfiguration, customFingerprints: customFingerprints, + customSigningEntities: customSigningEntities, customMirrors: customMirrors, customToolsVersion: customToolsVersion, customHostToolchain: customHostToolchain, @@ -537,6 +541,7 @@ public class Workspace { // optional customization, primarily designed for testing but also used in some cases by libSwiftPM consumers customRegistriesConfiguration: RegistryConfiguration?, customFingerprints: PackageFingerprintStorage?, + customSigningEntities: PackageSigningEntityStorage?, customMirrors: DependencyMirrors?, customToolsVersion: ToolsVersion?, customHostToolchain: UserToolchain?, @@ -598,6 +603,13 @@ public class Workspace { directoryPath: $0 ) } + + let signingEntities = customSigningEntities ?? location.sharedSigningEntitiesDirectory.map { + FilePackageSigningEntityStorage( + fileSystem: fileSystem, + directoryPath: $0 + ) + } let registriesConfiguration = try customRegistriesConfiguration ?? Workspace.Configuration.Registries( fileSystem: fileSystem, @@ -608,7 +620,9 @@ public class Workspace { let registryClient = customRegistryClient ?? RegistryClient( configuration: registriesConfiguration, fingerprintStorage: fingerprints, - fingerprintCheckingMode: configuration.fingerprintCheckingMode, + fingerprintCheckingMode: FingerprintCheckingMode.map(configuration.fingerprintCheckingMode), + signingEntityStorage: signingEntities, + signingEntityCheckingMode: SigningEntityCheckingMode.map(configuration.signingEntityCheckingMode), authorizationProvider: registryAuthorizationProvider ) @@ -3044,7 +3058,7 @@ extension Workspace: PackageContainerProvider { manifestLoader: self.manifestLoader, currentToolsVersion: self.currentToolsVersion, fingerprintStorage: self.fingerprints, - fingerprintCheckingMode: self.configuration.fingerprintCheckingMode, + fingerprintCheckingMode: FingerprintCheckingMode.map(self.configuration.fingerprintCheckingMode), observabilityScope: observabilityScope ) } @@ -3128,6 +3142,28 @@ extension Workspace: PackageContainerProvider { } } +extension FingerprintCheckingMode { + static func map(_ checkingMode: WorkspaceConfiguration.CheckingMode) -> FingerprintCheckingMode { + switch checkingMode { + case .strict: + return .strict + case .warn: + return .warn + } + } +} + +extension SigningEntityCheckingMode { + static func map(_ checkingMode: WorkspaceConfiguration.CheckingMode) -> SigningEntityCheckingMode { + switch checkingMode { + case .strict: + return .strict + case .warn: + return .warn + } + } +} + // MARK: - Source control repository management // FIXME: this mixes quite a bit of workspace logic with repository specific one diff --git a/Tests/PackageRegistryTests/RegistryClientTests.swift b/Tests/PackageRegistryTests/RegistryClientTests.swift index eac8370d659..4ee9702bd08 100644 --- a/Tests/PackageRegistryTests/RegistryClientTests.swift +++ b/Tests/PackageRegistryTests/RegistryClientTests.swift @@ -16,6 +16,7 @@ import PackageFingerprint import PackageLoading import PackageModel @testable import PackageRegistry +import PackageSigning import SPMTestSupport import TSCBasic import XCTest @@ -1033,6 +1034,8 @@ final class RegistryClientTests: XCTestCase { configuration: configuration, fingerprintStorage: fingerprintStorage, fingerprintCheckingMode: .strict, + signingEntityStorage: .none, + signingEntityCheckingMode: .strict, customHTTPClient: httpClient, customArchiverProvider: { fileSystem in MockArchiver(handler: { _, from, to, callback in @@ -1116,6 +1119,8 @@ final class RegistryClientTests: XCTestCase { configuration: configuration, fingerprintStorage: fingerprintStorage, fingerprintCheckingMode: .strict, // intended for this test; don't change + signingEntityStorage: .none, + signingEntityCheckingMode: .strict, customHTTPClient: httpClient, customArchiverProvider: { fileSystem in MockArchiver(handler: { _, from, to, callback in @@ -1205,6 +1210,8 @@ final class RegistryClientTests: XCTestCase { configuration: configuration, fingerprintStorage: fingerprintStorage, fingerprintCheckingMode: .warn, // intended for this test; don't change + signingEntityStorage: .none, + signingEntityCheckingMode: .strict, customHTTPClient: httpClient, customArchiverProvider: { fileSystem in MockArchiver(handler: { _, from, to, callback in @@ -1322,6 +1329,8 @@ final class RegistryClientTests: XCTestCase { configuration: configuration, fingerprintStorage: fingerprintStorage, fingerprintCheckingMode: .strict, + signingEntityStorage: .none, + signingEntityCheckingMode: .strict, customHTTPClient: httpClient, customArchiverProvider: { fileSystem in MockArchiver(handler: { _, from, to, callback in @@ -1445,6 +1454,8 @@ final class RegistryClientTests: XCTestCase { configuration: configuration, fingerprintStorage: fingerprintStorage, fingerprintCheckingMode: .strict, + signingEntityStorage: .none, + signingEntityCheckingMode: .strict, customHTTPClient: httpClient, customArchiverProvider: { fileSystem in MockArchiver(handler: { _, from, to, callback in @@ -1498,6 +1509,8 @@ final class RegistryClientTests: XCTestCase { configuration: configuration, fingerprintStorage: .none, fingerprintCheckingMode: .strict, + signingEntityStorage: .none, + signingEntityCheckingMode: .strict, customHTTPClient: httpClient ) @@ -1548,6 +1561,8 @@ final class RegistryClientTests: XCTestCase { configuration: configuration, fingerprintStorage: .none, fingerprintCheckingMode: .strict, + signingEntityStorage: .none, + signingEntityCheckingMode: .strict, customHTTPClient: httpClient ) @@ -1594,6 +1609,8 @@ final class RegistryClientTests: XCTestCase { configuration: configuration, fingerprintStorage: .none, fingerprintCheckingMode: .strict, + signingEntityStorage: .none, + signingEntityCheckingMode: .strict, customHTTPClient: httpClient ) @@ -2498,12 +2515,16 @@ func makeRegistryClient( httpClient: LegacyHTTPClient, authorizationProvider: AuthorizationProvider? = .none, fingerprintStorage: PackageFingerprintStorage = MockPackageFingerprintStorage(), - fingerprintCheckingMode: FingerprintCheckingMode = .strict + fingerprintCheckingMode: FingerprintCheckingMode = .strict, + signingEntityStorage: PackageSigningEntityStorage = MockPackageSigningEntityStorage(), + signingEntityCheckingMode: SigningEntityCheckingMode = .strict ) -> RegistryClient { RegistryClient( configuration: configuration, fingerprintStorage: fingerprintStorage, fingerprintCheckingMode: fingerprintCheckingMode, + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode, authorizationProvider: authorizationProvider, customHTTPClient: httpClient, customArchiverProvider: { _ in MockArchiver() } diff --git a/Tests/PackageRegistryTests/RegistryDownloadsManagerTests.swift b/Tests/PackageRegistryTests/RegistryDownloadsManagerTests.swift index 4ca6a848907..ee205c645ed 100644 --- a/Tests/PackageRegistryTests/RegistryDownloadsManagerTests.swift +++ b/Tests/PackageRegistryTests/RegistryDownloadsManagerTests.swift @@ -29,7 +29,8 @@ class RegistryDownloadsManagerTests: XCTestCase { filesystem: fs, identityResolver: DefaultIdentityResolver(), checksumAlgorithm: MockHashAlgorithm(), - fingerprintStorage: MockPackageFingerprintStorage() + fingerprintStorage: MockPackageFingerprintStorage(), + signingEntityStorage: MockPackageSigningEntityStorage() ) let package: PackageIdentity = .plain("test.\(UUID().uuidString)") @@ -160,7 +161,8 @@ class RegistryDownloadsManagerTests: XCTestCase { filesystem: fs, identityResolver: DefaultIdentityResolver(), checksumAlgorithm: MockHashAlgorithm(), - fingerprintStorage: MockPackageFingerprintStorage() + fingerprintStorage: MockPackageFingerprintStorage(), + signingEntityStorage: MockPackageSigningEntityStorage() ) let package: PackageIdentity = .plain("test.\(UUID().uuidString)") @@ -261,7 +263,8 @@ class RegistryDownloadsManagerTests: XCTestCase { filesystem: fs, identityResolver: DefaultIdentityResolver(), checksumAlgorithm: MockHashAlgorithm(), - fingerprintStorage: MockPackageFingerprintStorage() + fingerprintStorage: MockPackageFingerprintStorage(), + signingEntityStorage: MockPackageSigningEntityStorage() ) let downloadsPath = AbsolutePath.root.appending(components: "registry", "downloads") diff --git a/Tests/WorkspaceTests/RegistryPackageContainerTests.swift b/Tests/WorkspaceTests/RegistryPackageContainerTests.swift index 8782ec9f97a..818fcc5aebb 100644 --- a/Tests/WorkspaceTests/RegistryPackageContainerTests.swift +++ b/Tests/WorkspaceTests/RegistryPackageContainerTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2014-2020 Apple Inc. and the Swift project authors +// Copyright (c) 2014-2023 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 @@ -435,6 +435,8 @@ class RegistryPackageContainerTests: XCTestCase { configuration: configuration!, fingerprintStorage: fingerprintStorage, fingerprintCheckingMode: .strict, + signingEntityStorage: .none, + signingEntityCheckingMode: .strict, authorizationProvider: .none, customHTTPClient: LegacyHTTPClient(configuration: .init(), handler: { request, progress , completion in var pathComponents = request.url.pathComponents diff --git a/Tests/WorkspaceTests/WorkspaceTests.swift b/Tests/WorkspaceTests/WorkspaceTests.swift index e25da682c9a..5e6c5b555ae 100644 --- a/Tests/WorkspaceTests/WorkspaceTests.swift +++ b/Tests/WorkspaceTests/WorkspaceTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2014-2022 Apple Inc. and the Swift project authors +// Copyright (c) 2014-2023 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 @@ -16,6 +16,7 @@ import PackageFingerprint import PackageLoading import PackageModel import PackageRegistry +import PackageSigning import SourceControl import SPMBuildCore import SPMTestSupport @@ -12760,6 +12761,8 @@ final class WorkspaceTests: XCTestCase { identityResolver: IdentityResolver? = .none, fingerprintStorage: PackageFingerprintStorage? = .none, fingerprintCheckingMode: FingerprintCheckingMode = .strict, + signingEntityStorage: PackageSigningEntityStorage? = .none, + signingEntityCheckingMode: SigningEntityCheckingMode = .strict, authorizationProvider: AuthorizationProvider? = .none, releasesRequestHandler: LegacyHTTPClient.Handler? = .none, versionMetadataRequestHandler: LegacyHTTPClient.Handler? = .none, @@ -12865,11 +12868,14 @@ final class WorkspaceTests: XCTestCase { } }) let fingerprintStorage = fingerprintStorage ?? MockPackageFingerprintStorage() + let signingEntityStorage = signingEntityStorage ?? MockPackageSigningEntityStorage() return RegistryClient( configuration: configuration!, fingerprintStorage: fingerprintStorage, fingerprintCheckingMode: fingerprintCheckingMode, + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode, authorizationProvider: authorizationProvider, customHTTPClient: LegacyHTTPClient(configuration: .init(), handler: { request, progress , completion in switch request.url { From f90ede458d2550b9d8cca8acda4e9f095f5c29ed Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Mon, 27 Feb 2023 15:08:26 -0800 Subject: [PATCH 07/20] Rename tofu.check to tofu.validate --- Sources/PackageRegistry/ChecksumTOFU.swift | 2 +- Sources/PackageRegistry/RegistryClient.swift | 2 +- .../PackageVersionChecksumTOFUTests.swift | 22 +++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Sources/PackageRegistry/ChecksumTOFU.swift b/Sources/PackageRegistry/ChecksumTOFU.swift index 72084e9bfe7..50223ee3064 100644 --- a/Sources/PackageRegistry/ChecksumTOFU.swift +++ b/Sources/PackageRegistry/ChecksumTOFU.swift @@ -34,7 +34,7 @@ struct PackageVersionChecksumTOFU { self.registryClient = registryClient } - func check( + func validate( registry: Registry, package: PackageIdentity.RegistryIdentity, version: Version, diff --git a/Sources/PackageRegistry/RegistryClient.swift b/Sources/PackageRegistry/RegistryClient.swift index 1bcff5e563d..1f7bcc08d02 100644 --- a/Sources/PackageRegistry/RegistryClient.swift +++ b/Sources/PackageRegistry/RegistryClient.swift @@ -745,7 +745,7 @@ public final class RegistryClient: Cancellable { let contents = try fileSystem.readFileContents(downloadPath) let actualChecksum = checksumAlgorithm.hash(contents).hexadecimalRepresentation - self.checksumTOFU.check( + self.checksumTOFU.validate( registry: registry, package: package, version: version, diff --git a/Tests/PackageRegistryTests/PackageVersionChecksumTOFUTests.swift b/Tests/PackageRegistryTests/PackageVersionChecksumTOFUTests.swift index 2b38a76c388..8a70753006b 100644 --- a/Tests/PackageRegistryTests/PackageVersionChecksumTOFUTests.swift +++ b/Tests/PackageRegistryTests/PackageVersionChecksumTOFUTests.swift @@ -95,7 +95,7 @@ final class PackageVersionChecksumTOFUTests: XCTestCase { // so we fetch metadata to get the expected checksum, // then save it to storage for future reference. XCTAssertNoThrow( - try tofu.check( + try tofu.validate( registry: registry, package: package, version: version, @@ -190,7 +190,7 @@ final class PackageVersionChecksumTOFUTests: XCTestCase { // from value in storage, and because of .strict mode, // an error is thrown. XCTAssertThrowsError( - try tofu.check( + try tofu.validate( registry: registry, package: package, version: version, @@ -277,7 +277,7 @@ final class PackageVersionChecksumTOFUTests: XCTestCase { // from value in storage, but because of .warn mode, // no error is thrown. XCTAssertNoThrow( - try tofu.check( + try tofu.validate( registry: registry, package: package, version: version, @@ -335,7 +335,7 @@ final class PackageVersionChecksumTOFUTests: XCTestCase { // from value in storage, and because of .strict mode, // an error is thrown. XCTAssertThrowsError( - try tofu.check( + try tofu.validate( registry: registry, package: package, version: version, @@ -391,7 +391,7 @@ final class PackageVersionChecksumTOFUTests: XCTestCase { // from value in storage, and because of .strict mode, // an error is thrown. XCTAssertThrowsError( - try tofu.check( + try tofu.validate( registry: registry, package: package, version: version, @@ -441,7 +441,7 @@ final class PackageVersionChecksumTOFUTests: XCTestCase { // from value in storage, and because of .strict mode, // an error is thrown. XCTAssertThrowsError( - try tofu.check( + try tofu.validate( registry: registry, package: package, version: version, @@ -497,7 +497,7 @@ final class PackageVersionChecksumTOFUTests: XCTestCase { // Checksum for package version found in storage, // so we just compare that with the given checksum. XCTAssertNoThrow( - try tofu.check( + try tofu.validate( registry: registry, package: package, version: version, @@ -551,7 +551,7 @@ final class PackageVersionChecksumTOFUTests: XCTestCase { // Since the checksums don't match, and because of // .strict mode, an error is thrown. XCTAssertThrowsError( - try tofu.check( + try tofu.validate( registry: registry, package: package, version: version, @@ -611,7 +611,7 @@ final class PackageVersionChecksumTOFUTests: XCTestCase { // The checksums don't match, but because of // .warn mode, no error is thrown. XCTAssertNoThrow( - try tofu.check( + try tofu.validate( registry: registry, package: package, version: version, @@ -628,7 +628,7 @@ final class PackageVersionChecksumTOFUTests: XCTestCase { } extension PackageVersionChecksumTOFU { - fileprivate func check( + fileprivate func validate( registry: Registry, package: PackageIdentity.RegistryIdentity, version: Version, @@ -636,7 +636,7 @@ extension PackageVersionChecksumTOFU { observabilityScope: ObservabilityScope? = nil ) throws { try tsc_await { - self.check( + self.validate( registry: registry, package: package, version: version, From fa0630344393f585cf8708e14b9307a66ba92893 Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Mon, 27 Feb 2023 17:48:14 -0800 Subject: [PATCH 08/20] Use registry identity in configuration --- .../RegistryConfiguration.swift | 17 +++---- .../PackageRegistry/SignatureValidation.swift | 8 +++ .../PackageRegistry/SigningEntityTOFU.swift | 8 +++ .../RegistryConfigurationTests.swift | 50 +++++++++---------- 4 files changed, 47 insertions(+), 36 deletions(-) create mode 100644 Sources/PackageRegistry/SignatureValidation.swift create mode 100644 Sources/PackageRegistry/SigningEntityTOFU.swift diff --git a/Sources/PackageRegistry/RegistryConfiguration.swift b/Sources/PackageRegistry/RegistryConfiguration.swift index 36e7202a991..b149dc14050 100644 --- a/Sources/PackageRegistry/RegistryConfiguration.swift +++ b/Sources/PackageRegistry/RegistryConfiguration.swift @@ -55,7 +55,7 @@ public struct RegistryConfiguration: Hashable { } public func registry(for scope: PackageIdentity.Scope) -> Registry? { - self.scopedRegistries[scope] ?? self.defaultRegistry + self.scopedRegistries[scope] ?? self.defaultRegistry } public var explicitlyConfigured: Bool { @@ -67,14 +67,10 @@ public struct RegistryConfiguration: Hashable { return self.registryAuthentication[host] } - public func signing(for package: PackageIdentity, registry: Registry) throws -> Security.Signing { - guard let registryIdentity = package.registry else { - throw StringError("Only package identity in . format is supported: '\(package)'") - } - + public func signing(for package: PackageIdentity.RegistryIdentity, registry: Registry) -> Security.Signing { let global = self.security?.default.signing let registryOverrides = registry.url.host.flatMap { host in self.security?.registryOverrides[host]?.signing } - let scopeOverrides = self.security?.scopeOverrides[registryIdentity.scope]?.signing + let scopeOverrides = self.security?.scopeOverrides[package.scope]?.signing let packageOverrides = self.security?.packageOverrides[package]?.signing var signing = Security.Signing.default @@ -117,7 +113,7 @@ extension RegistryConfiguration { public var `default`: Global public var registryOverrides: [String: RegistryOverride] public var scopeOverrides: [PackageIdentity.Scope: ScopePackageOverride] - public var packageOverrides: [PackageIdentity: ScopePackageOverride] + public var packageOverrides: [PackageIdentity.RegistryIdentity: ScopePackageOverride] public init() { self.default = Global() @@ -412,10 +408,9 @@ extension RegistryConfiguration.Security: Codable { [String: ScopePackageOverride].self, forKey: .packageOverrides ) ?? [:] - var packageOverrides: [PackageIdentity: ScopePackageOverride] = [:] + var packageOverrides: [PackageIdentity.RegistryIdentity: ScopePackageOverride] = [:] for (key, packageOverride) in packageOverridesContainer { - let packageIdentity = PackageIdentity.plain(key) - guard packageIdentity.isRegistry else { + guard let packageIdentity = PackageIdentity.plain(key).registry else { throw DecodingError.dataCorruptedError( forKey: .packageOverrides, in: container, diff --git a/Sources/PackageRegistry/SignatureValidation.swift b/Sources/PackageRegistry/SignatureValidation.swift new file mode 100644 index 00000000000..024c973e47b --- /dev/null +++ b/Sources/PackageRegistry/SignatureValidation.swift @@ -0,0 +1,8 @@ +// +// File.swift +// +// +// Created by Yim Lee on 2/27/23. +// + +import Foundation diff --git a/Sources/PackageRegistry/SigningEntityTOFU.swift b/Sources/PackageRegistry/SigningEntityTOFU.swift new file mode 100644 index 00000000000..024c973e47b --- /dev/null +++ b/Sources/PackageRegistry/SigningEntityTOFU.swift @@ -0,0 +1,8 @@ +// +// File.swift +// +// +// Created by Yim Lee on 2/27/23. +// + +import Foundation diff --git a/Tests/PackageRegistryTests/RegistryConfigurationTests.swift b/Tests/PackageRegistryTests/RegistryConfigurationTests.swift index 1b9f0307c47..31b83659d08 100644 --- a/Tests/PackageRegistryTests/RegistryConfigurationTests.swift +++ b/Tests/PackageRegistryTests/RegistryConfigurationTests.swift @@ -80,7 +80,7 @@ final class RegistryConfigurationTests: XCTestCase { scopeOverride.signing?.includeDefaultTrustedRootCertificates = false security.scopeOverrides[scope] = scopeOverride // packageOverrides - let packageIdentity = PackageIdentity.plain("mona.LinkedList") + let packageIdentity = PackageIdentity.plain("mona.LinkedList").registry! var packageOverride = RegistryConfiguration.Security.ScopePackageOverride() packageOverride.signing = RegistryConfiguration.Security.ScopePackageOverride.Signing() packageOverride.signing?.trustedRootCertificatesPath = "/mona/LinkedList/roots" @@ -244,12 +244,12 @@ final class RegistryConfigurationTests: XCTestCase { false ) XCTAssertEqual( - configuration.security?.packageOverrides[PackageIdentity.plain("mona.LinkedList")]?.signing? + configuration.security?.packageOverrides[PackageIdentity.plain("mona.LinkedList").registry!]?.signing? .trustedRootCertificatesPath, "/mona/LinkedList/roots" ) XCTAssertEqual( - configuration.security?.packageOverrides[PackageIdentity.plain("mona.LinkedList")]?.signing? + configuration.security?.packageOverrides[PackageIdentity.plain("mona.LinkedList").registry!]?.signing? .includeDefaultTrustedRootCertificates, false ) @@ -388,9 +388,9 @@ final class RegistryConfigurationTests: XCTestCase { let configuration = try decoder.decode(RegistryConfiguration.self, from: json) - let package = PackageIdentity.plain("mona.LinkedList") + let package = PackageIdentity.plain("mona.LinkedList").registry! let registry = Registry(url: "https://packages.example.com", supportsAvailability: false) - let signing = try configuration.signing(for: package, registry: registry) + let signing = configuration.signing(for: package, registry: registry) XCTAssertEqual(signing.onUnsigned, .prompt) XCTAssertEqual(signing.onUntrustedCertificate, .prompt) @@ -421,9 +421,9 @@ final class RegistryConfigurationTests: XCTestCase { let configuration = try decoder.decode(RegistryConfiguration.self, from: json) - let package = PackageIdentity.plain("mona.LinkedList") + let package = PackageIdentity.plain("mona.LinkedList").registry! let registry = Registry(url: "https://packages.example.com", supportsAvailability: false) - let signing = try configuration.signing(for: package, registry: registry) + let signing = configuration.signing(for: package, registry: registry) XCTAssertEqual(signing.onUnsigned, .error) XCTAssertEqual(signing.onUntrustedCertificate, .prompt) @@ -461,9 +461,9 @@ final class RegistryConfigurationTests: XCTestCase { let configuration = try decoder.decode(RegistryConfiguration.self, from: json) - let package = PackageIdentity.plain("mona.LinkedList") + let package = PackageIdentity.plain("mona.LinkedList").registry! let registry = Registry(url: "https://packages.example.com", supportsAvailability: false) - let signing = try configuration.signing(for: package, registry: registry) + let signing = configuration.signing(for: package, registry: registry) XCTAssertEqual(signing.onUnsigned, .prompt) XCTAssertEqual(signing.onUntrustedCertificate, .warn) @@ -497,9 +497,9 @@ final class RegistryConfigurationTests: XCTestCase { let configuration = try decoder.decode(RegistryConfiguration.self, from: json) - let package = PackageIdentity.plain("mona.LinkedList") + let package = PackageIdentity.plain("mona.LinkedList").registry! let registry = Registry(url: "https://packages.example.com", supportsAvailability: false) - let signing = try configuration.signing(for: package, registry: registry) + let signing = configuration.signing(for: package, registry: registry) XCTAssertEqual(signing.onUnsigned, .prompt) XCTAssertEqual(signing.onUntrustedCertificate, .prompt) @@ -533,9 +533,9 @@ final class RegistryConfigurationTests: XCTestCase { let configuration = try decoder.decode(RegistryConfiguration.self, from: json) - let package = PackageIdentity.plain("mona.LinkedList") + let package = PackageIdentity.plain("mona.LinkedList").registry! let registry = Registry(url: "https://packages.example.com", supportsAvailability: false) - let signing = try configuration.signing(for: package, registry: registry) + let signing = configuration.signing(for: package, registry: registry) XCTAssertEqual(signing.onUnsigned, .prompt) XCTAssertEqual(signing.onUntrustedCertificate, .prompt) @@ -544,7 +544,7 @@ final class RegistryConfigurationTests: XCTestCase { XCTAssertEqual(signing.validationChecks?.certificateExpiration, .disabled) XCTAssertEqual(signing.validationChecks?.certificateRevocation, .disabled) } - + func testGetSigning_multipleOverrides() throws { let json = #""" { @@ -584,27 +584,27 @@ final class RegistryConfigurationTests: XCTestCase { let configuration = try decoder.decode(RegistryConfiguration.self, from: json) // Package override wins - var package = PackageIdentity.plain("mona.LinkedList") + var package = PackageIdentity.plain("mona.LinkedList").registry! var registry = Registry(url: "https://packages.example.com", supportsAvailability: false) - var signing = try configuration.signing(for: package, registry: registry) + var signing = configuration.signing(for: package, registry: registry) XCTAssertEqual(signing.trustedRootCertificatesPath, "/mona/linkedlist/roots") - + // No package override, closest match is scope override - package = PackageIdentity.plain("mona.Trie") + package = PackageIdentity.plain("mona.Trie").registry! registry = Registry(url: "https://packages.example.com", supportsAvailability: false) - signing = try configuration.signing(for: package, registry: registry) + signing = configuration.signing(for: package, registry: registry) XCTAssertEqual(signing.trustedRootCertificatesPath, "/mona/roots") - + // No package override, closest match is registry override - package = PackageIdentity.plain("foo.bar") + package = PackageIdentity.plain("foo.bar").registry! registry = Registry(url: "https://packages.example.com", supportsAvailability: false) - signing = try configuration.signing(for: package, registry: registry) + signing = configuration.signing(for: package, registry: registry) XCTAssertEqual(signing.trustedRootCertificatesPath, "/foo/roots") - + // Global override - package = PackageIdentity.plain("foo.bar") + package = PackageIdentity.plain("foo.bar").registry! registry = Registry(url: "https://other.example.com", supportsAvailability: false) - signing = try configuration.signing(for: package, registry: registry) + signing = configuration.signing(for: package, registry: registry) XCTAssertEqual(signing.trustedRootCertificatesPath, "/custom/roots") } } From c41b8990bc3f3e0240dea0db886dff5d71de9528 Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Tue, 28 Feb 2023 00:48:57 -0800 Subject: [PATCH 09/20] Implement signature validation and signing entity TOFU flow. Need tests --- CMakeLists.txt | 3 +- Sources/PackageModel/PackageIdentity.swift | 2 +- Sources/PackageRegistry/CMakeLists.txt | 4 +- Sources/PackageRegistry/ChecksumTOFU.swift | 4 +- Sources/PackageRegistry/RegistryClient.swift | 76 +++++- .../PackageRegistry/SignatureValidation.swift | 252 +++++++++++++++++- .../PackageRegistry/SigningEntityTOFU.swift | 246 ++++++++++++++++- Sources/PackageSigning/CMakeLists.txt | 1 + .../PackageSigning/SignatureProvider.swift | 2 +- .../FilePackageSigningEntityStorage.swift | 11 - .../PackageSigningEntityStorage.swift | 21 ++ 11 files changed, 587 insertions(+), 35 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3d0db5ce519..28f9ee7378a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ # This source file is part of the Swift open source project # -# Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +# Copyright (c) 2014 - 2023 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 @@ -54,6 +54,7 @@ if(FIND_PM_DEPS) find_package(ArgumentParser CONFIG REQUIRED) find_package(SwiftDriver CONFIG REQUIRED) find_package(SwiftCollections CONFIG REQUIRED) + find_package(SwiftCrypto CONFIG REQUIRED) endif() find_package(dispatch QUIET) diff --git a/Sources/PackageModel/PackageIdentity.swift b/Sources/PackageModel/PackageIdentity.swift index d202b288738..327211d1456 100644 --- a/Sources/PackageModel/PackageIdentity.swift +++ b/Sources/PackageModel/PackageIdentity.swift @@ -74,7 +74,7 @@ public struct PackageIdentity: CustomStringConvertible, Sendable { self.registry != nil } - public struct RegistryIdentity: CustomStringConvertible { + public struct RegistryIdentity: Hashable, CustomStringConvertible { public let scope: PackageIdentity.Scope public let name: PackageIdentity.Name public let underlying: PackageIdentity diff --git a/Sources/PackageRegistry/CMakeLists.txt b/Sources/PackageRegistry/CMakeLists.txt index d2bbe9b8729..c0d1dd67bc4 100644 --- a/Sources/PackageRegistry/CMakeLists.txt +++ b/Sources/PackageRegistry/CMakeLists.txt @@ -7,11 +7,13 @@ # See http://swift.org/CONTRIBUTORS.txt for Swift project authors add_library(PackageRegistry STATIC + ChecksumTOFU.swift Registry.swift RegistryConfiguration.swift RegistryClient.swift RegistryDownloadsManager.swift - ChecksumTOFU.swift) + SignatureValidation.swift + SigningEntityTOFU.swift) target_link_libraries(PackageRegistry PUBLIC Basics PackageFingerprint diff --git a/Sources/PackageRegistry/ChecksumTOFU.swift b/Sources/PackageRegistry/ChecksumTOFU.swift index 50223ee3064..39256ba723c 100644 --- a/Sources/PackageRegistry/ChecksumTOFU.swift +++ b/Sources/PackageRegistry/ChecksumTOFU.swift @@ -104,9 +104,7 @@ struct PackageVersionChecksumTOFU { ) { result in switch result { case .success(let metadata): - guard let sourceArchive = metadata.resources - .first(where: { $0.name == "source-archive" }) - else { + guard let sourceArchive = metadata.sourceArchive else { return completion(.failure(RegistryError.missingSourceArchive)) } diff --git a/Sources/PackageRegistry/RegistryClient.swift b/Sources/PackageRegistry/RegistryClient.swift index 1f7bcc08d02..0450994c38b 100644 --- a/Sources/PackageRegistry/RegistryClient.swift +++ b/Sources/PackageRegistry/RegistryClient.swift @@ -26,21 +26,26 @@ import struct TSCUtility.Version public final class RegistryClient: Cancellable { private static let apiVersion: APIVersion = .v1 private static let availabilityCacheTTL: DispatchTimeInterval = .seconds(5 * 60) + private static let metadataCacheTTL: DispatchTimeInterval = .seconds(60 * 60) private let configuration: RegistryConfiguration private let archiverProvider: (FileSystem) -> Archiver private let httpClient: LegacyHTTPClient private let authorizationProvider: LegacyHTTPClientConfiguration.AuthorizationProvider? - private let signingEntityStorage: PackageSigningEntityStorage? - private let signingEntityCheckingMode: SigningEntityCheckingMode private let jsonDecoder: JSONDecoder private var checksumTOFU: PackageVersionChecksumTOFU! + private let signingEntityTOFU: PackageSigningEntityTOFU private let availabilityCache = ThreadSafeKeyValueStore< URL, (status: Result, expires: DispatchTime) >() + + private let metadataCache = ThreadSafeKeyValueStore< + MetadataCacheKey, + (metadata: Serialization.VersionMetadata, expires: DispatchTime) + >() public init( configuration: RegistryConfiguration, @@ -80,10 +85,12 @@ public final class RegistryClient: Cancellable { self.httpClient = customHTTPClient ?? LegacyHTTPClient() self.archiverProvider = customArchiverProvider ?? { fileSystem in ZipArchiver(fileSystem: fileSystem) } - self.signingEntityStorage = signingEntityStorage - self.signingEntityCheckingMode = signingEntityCheckingMode self.jsonDecoder = JSONDecoder.makeWithDefaults() + self.signingEntityTOFU = PackageSigningEntityTOFU( + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode + ) self.checksumTOFU = PackageVersionChecksumTOFU( fingerprintStorage: fingerprintStorage, fingerprintCheckingMode: fingerprintCheckingMode, @@ -321,7 +328,6 @@ public final class RegistryClient: Cancellable { } } - // TODO: add caching // marked internal for testing func _getRawPackageVersionMetadata( registry: Registry, @@ -333,6 +339,11 @@ public final class RegistryClient: Cancellable { completion: @escaping (Result) -> Void ) { let completion = self.makeAsync(completion, on: callbackQueue) + + let cacheKey = MetadataCacheKey(registry: registry, package: package) + if let cached = self.metadataCache[cacheKey], cached.expires < .now() { + return completion(.success(cached.metadata)) + } guard var components = URLComponents(url: registry.url, resolvingAgainstBaseURL: true) else { return completion(.failure(RegistryError.invalidURL(registry.url))) @@ -357,10 +368,12 @@ public final class RegistryClient: Cancellable { result.tryMap { response in switch response.statusCode { case 200: - return try response.parseJSON( + let metadata = try response.parseJSON( Serialization.VersionMetadata.self, decoder: self.jsonDecoder ) + self.metadataCache[cacheKey] = (metadata: metadata, expires: .now() + Self.metadataCacheTTL) + return metadata case 404: throw RegistryError.packageVersionNotFound default: @@ -1182,6 +1195,11 @@ public final class RegistryClient: Cancellable { options.authorizationProvider = self.authorizationProvider return options } + + private struct MetadataCacheKey: Hashable { + let registry: Registry + let package: PackageIdentity.RegistryIdentity + } } public enum RegistryError: Error, CustomStringConvertible { @@ -1216,6 +1234,18 @@ public enum RegistryError: Error, CustomStringConvertible { case registryNotAvailable(Registry) case packageNotFound case packageVersionNotFound + case sourceArchiveNotSigned(registry: Registry, package: PackageIdentity, version: Version) + case failedLoadingSignature + case failedRetrievingSourceArchiveSignature(registry: Registry, package: PackageIdentity, version: Version, error: Error) + case missingConfiguration(details: String) + case missingSignatureFormat + case unknownSignatureFormat(String) + case invalidSignature(reason: String) + case invalidSigningCertificate(reason: String) + case signerNotTrusted + case failedToValidateSignature(Error) + case signingEntityForReleaseHasChanged(package: PackageIdentity, version: Version, latest: SigningEntity?, previous: SigningEntity) + case signingEntityForPackageHasChanged(package: PackageIdentity, latest: SigningEntity?, previous: SigningEntity) public var description: String { switch self { @@ -1280,11 +1310,35 @@ public enum RegistryError: Error, CustomStringConvertible { case .forbidden: return "Forbidden" case .registryNotAvailable(let registry): - return "registry at '\(registry.url)' is not available at this time, please try again later" + return "Registry at '\(registry.url)' is not available at this time, please try again later" case .packageNotFound: - return "package not found on registry" + return "Package not found on registry" case .packageVersionNotFound: - return "package version not found on registry" + return "Package version not found on registry" + case .sourceArchiveNotSigned(let registry, let packageIdentity, let version): + return "'\(packageIdentity)@\(version)' source archive from '\(registry)' is not signed" + case .failedLoadingSignature: + return "Failed loading signature for validation" + case .failedRetrievingSourceArchiveSignature(let registry, let packageIdentity, let version, let error): + return "Failed retrieving '\(packageIdentity)@\(version)' source archive signature from '\(registry)': \(error)" + case .missingConfiguration(let details): + return "Unable to proceed because of missing configuration: \(details)" + case .missingSignatureFormat: + return "Missing signature format" + case .unknownSignatureFormat(let format): + return "Unknown signature format: \(format)" + case .invalidSignature(let reason): + return "Signature is invalid: \(reason)" + case .invalidSigningCertificate(let reason): + return "The signing certificate is invalid: \(reason)" + case .signerNotTrusted: + return "The signer is not trusted" + case .failedToValidateSignature(let error): + return "Failed to validate signature: \(error)" + case .signingEntityForReleaseHasChanged(let package, let version, let latest, let previous): + return "The signing entity '\(String(describing: latest))' for '\(package)@\(version)' is different from the previously recorded value '\(previous)'" + case .signingEntityForPackageHasChanged(let package, let latest, let previous): + return "The signing entity '\(String(describing: latest))' for '\(package)' is different from the previously recorded value '\(previous)'" } } } @@ -1613,6 +1667,10 @@ extension RegistryClient { public let version: String public let resources: [Resource] public let metadata: AdditionalMetadata? + + var sourceArchive: Resource? { + self.resources.first(where: { $0.name == "source-archive" }) + } public init( id: String, diff --git a/Sources/PackageRegistry/SignatureValidation.swift b/Sources/PackageRegistry/SignatureValidation.swift index 024c973e47b..17553ec2fe7 100644 --- a/Sources/PackageRegistry/SignatureValidation.swift +++ b/Sources/PackageRegistry/SignatureValidation.swift @@ -1,8 +1,252 @@ +//===----------------------------------------------------------------------===// // -// File.swift -// +// This source file is part of the Swift open source project // -// Created by Yim Lee on 2/27/23. +// Copyright (c) 2023 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 the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Dispatch +import struct Foundation.Data + +import Basics +import PackageModel +import PackageSigning + +import struct TSCUtility.Version + +struct SignatureValidation { + private let registryClient: RegistryClient + private let signingEntityTOFU: PackageSigningEntityTOFU + + init( + registryClient: RegistryClient, + signingEntityStorage: PackageSigningEntityStorage?, + signingEntityCheckingMode: SigningEntityCheckingMode + ) { + self.registryClient = registryClient + self.signingEntityTOFU = PackageSigningEntityTOFU( + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode + ) + } + + func validate( + registry: Registry, + package: PackageIdentity.RegistryIdentity, + version: Version, + content: Data, + configuration: RegistryConfiguration.Security.Signing, + timeout: DispatchTimeInterval?, + observabilityScope: ObservabilityScope, + callbackQueue: DispatchQueue, + completion: @escaping (Result) -> Void + ) { + self.getAndValidateSignature( + registry: registry, + package: package, + version: version, + content: content, + configuration: configuration, + timeout: timeout, + observabilityScope: observabilityScope, + callbackQueue: callbackQueue + ) { result in + completion( + result.tryMap { signingEntity in + self.signingEntityTOFU.validate( + package: package, + version: version, + signingEntity: signingEntity, + observabilityScope: observabilityScope, + callbackQueue: callbackQueue, + completion: completion + ) + } + ) + } + } + + private func getAndValidateSignature( + registry: Registry, + package: PackageIdentity.RegistryIdentity, + version: Version, + content: Data, + configuration: RegistryConfiguration.Security.Signing, + timeout: DispatchTimeInterval?, + observabilityScope: ObservabilityScope, + callbackQueue: DispatchQueue, + completion: @escaping (Result) -> Void + ) { + self.getSourceArchiveSignature( + registry: registry, + package: package, + version: version, + timeout: timeout, + observabilityScope: observabilityScope, + callbackQueue: callbackQueue + ) { result in + switch result { + case .success(let signature): + self.validateSignature( + registry: registry, + package: package, + version: version, + signature: signature.data, + signatureFormat: signature.format, + content: content, + configuration: configuration, + observabilityScope: observabilityScope, + completion: completion + ) + case .failure(let error): + guard let onUnsigned = configuration.onUnsigned else { + return completion(.failure( + RegistryError + .missingConfiguration(details: "security.signing.onUnsigned") + )) + } + + switch onUnsigned { + case .error: + completion(.failure(error)) + case .prompt: + if case RegistryError.sourceArchiveNotSigned = error { + // Source archive is not signed + // TODO: Call delegate to prompt user to continue with unsigned package or error. + } else { + // Cannot determine if source archive is signed or not + // TODO: Call delegate to prompt user to continue with package as if it were unsigned or error. + } + case .warn: + observabilityScope.emit(warning: "\(error)") + completion(.success(nil)) + case .silentAllow: + // Continue without logging + completion(.success(nil)) + } + } + } + } + + private func validateSignature( + registry: Registry, + package: PackageIdentity.RegistryIdentity, + version: Version, + signature: Data, + signatureFormat: SignatureFormat, + content: Data, + configuration: RegistryConfiguration.Security.Signing, + observabilityScope: ObservabilityScope, + completion: @escaping (Result) -> Void + ) { + Task { + do { + let signatureStatus = try await SignatureProvider.status( + of: signature, + for: content, + in: signatureFormat, + // TODO: load trusted roots (trustedRootCertificatesPath, includeDefaultTrustedRootCertificates) + // TODO: build policy set (validationChecks) + verifierConfiguration: .init(), + observabilityScope: observabilityScope + ) + + switch signatureStatus { + case .valid(let signingEntity): + completion(.success(signingEntity)) + case .invalid(let reason): + completion(.failure(RegistryError.invalidSignature(reason: reason))) + case .certificateInvalid(let reason): + completion(.failure(RegistryError.invalidSigningCertificate(reason: reason))) + case .certificateNotTrusted: + guard let onUntrusted = configuration.onUntrustedCertificate else { + return completion(.failure( + RegistryError + .missingConfiguration(details: "security.signing.onUntrustedCertificate") + )) + } + + switch onUntrusted { + case .error: + // TODO: populate error with signer detail + completion(.failure(RegistryError.signerNotTrusted)) + case .prompt: + // TODO: Call delegate to prompt user to continue with package or error. + () + case .warn: + // TODO: populate error with signer detail + observabilityScope.emit(warning: "\(RegistryError.signerNotTrusted)") + completion(.success(nil)) + case .silentAllow: + // Continue without logging + completion(.success(nil)) + } + } + } catch { + completion(.failure(RegistryError.failedToValidateSignature(error))) + } + } + } -import Foundation + private func getSourceArchiveSignature( + registry: Registry, + package: PackageIdentity.RegistryIdentity, + version: Version, + timeout: DispatchTimeInterval?, + observabilityScope: ObservabilityScope, + callbackQueue: DispatchQueue, + completion: @escaping (Result<(data: Data, format: SignatureFormat), Error>) -> Void + ) { + self.registryClient.getRawPackageVersionMetadata( + registry: registry, + package: package, + version: version, + timeout: timeout, + observabilityScope: observabilityScope, + callbackQueue: callbackQueue + ) { result in + completion( + result.tryMap { metadata in + guard let sourceArchive = metadata.sourceArchive else { + throw RegistryError.missingSourceArchive + } + guard let signatureBase64Encoded = sourceArchive.signing?.signatureBase64Encoded else { + throw RegistryError.sourceArchiveNotSigned( + registry: registry, + package: package.underlying, + version: version + ) + } + guard let signatureData = Data(base64Encoded: signatureBase64Encoded) else { + throw RegistryError.failedLoadingSignature + } + guard let signatureFormatString = sourceArchive.signing?.signatureFormat else { + throw RegistryError.missingSignatureFormat + } + guard let signatureFormat = SignatureFormat(rawValue: signatureFormatString) else { + throw RegistryError.unknownSignatureFormat(signatureFormatString) + } + return (signatureData, signatureFormat) + }.mapError { error in + let actualError: Error + if case RegistryError.failedRetrievingReleaseInfo(_, _, _, let error) = error { + actualError = error + } else { + actualError = error + } + return RegistryError.failedRetrievingSourceArchiveSignature( + registry: registry, + package: package.underlying, + version: version, + error: actualError + ) + } + ) + } + } +} diff --git a/Sources/PackageRegistry/SigningEntityTOFU.swift b/Sources/PackageRegistry/SigningEntityTOFU.swift index 024c973e47b..c37b012a255 100644 --- a/Sources/PackageRegistry/SigningEntityTOFU.swift +++ b/Sources/PackageRegistry/SigningEntityTOFU.swift @@ -1,8 +1,246 @@ +//===----------------------------------------------------------------------===// // -// File.swift -// +// This source file is part of the Swift open source project // -// Created by Yim Lee on 2/27/23. +// Copyright (c) 2023 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 the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Dispatch + +import Basics +import PackageModel +import PackageSigning + +import struct TSCUtility.Version + +struct PackageSigningEntityTOFU { + private let signingEntityStorage: PackageSigningEntityStorage? + private let signingEntityCheckingMode: SigningEntityCheckingMode + + init( + signingEntityStorage: PackageSigningEntityStorage?, + signingEntityCheckingMode: SigningEntityCheckingMode + ) { + self.signingEntityStorage = signingEntityStorage + self.signingEntityCheckingMode = signingEntityCheckingMode + } + + func validate( + package: PackageIdentity.RegistryIdentity, + version: Version, + signingEntity: SigningEntity?, + observabilityScope: ObservabilityScope, + callbackQueue: DispatchQueue, + completion: @escaping (Result) -> Void + ) { + guard let signingEntityStorage = self.signingEntityStorage else { + return completion(.success(())) + } + + signingEntityStorage.get( + package: package.underlying, + observabilityScope: observabilityScope, + callbackQueue: callbackQueue + ) { result in + switch result { + case .success(let signerVersions): + self.isSigningEntityOK( + package: package, + version: version, + signingEntity: signingEntity, + signerVersions: signerVersions, + observabilityScope: observabilityScope + ) { isOKResult in + switch isOKResult { + case .success(let shouldWrite): + guard shouldWrite, let signingEntity = signingEntity else { + return completion(.success(())) + } + self.writeToStorage( + package: package, + version: version, + signingEntity: signingEntity, + observabilityScope: observabilityScope, + callbackQueue: callbackQueue, + completion: completion + ) + case .failure(let error): + completion(.failure(error)) + } + } + case .failure(let error): + observabilityScope.emit(error: "Failed to get signing entity for \(package) from storage: \(error)") + completion(.failure(error)) + } + } + } + + private func isSigningEntityOK( + package: PackageIdentity.RegistryIdentity, + version: Version, + signingEntity: SigningEntity?, + signerVersions: [SigningEntity: Set], + observabilityScope: ObservabilityScope, + completion: @escaping (Result) -> Void + ) { + // Package is never signed. + // If signingEntity is nil, it means package remains unsigned, which is OK. (none -> none) + // Otherwise, we are now assigning a signing entity, which is also OK. (none -> some) + if signerVersions.isEmpty { + return completion(.success(true)) + } + + // If we get to this point, it means we have seen a signed version of the package. + + // TODO: It's possible that some of the signing entity changes are legitimate: + // e.g., change of package ownership, package author decides to stop signing releases, etc. + // Instead of failing, we should allow and prompt user to add/replace/remove signing entity. + + if let signerForVersion = signerVersions.signingEntity(of: version) { + // We recorded a different signer for the given version before. This is NOT OK. + guard signerForVersion == signingEntity else { + return self.handleSigningEntityChanged( + package: package, + version: version, + latest: signingEntity, + existing: signerForVersion, + observabilityScope: observabilityScope + ) { result in + completion(result.tryMap { false }) + } + } + // Signer remains the same, which could be nil, for the version. + completion(.success(false)) + } + + switch signingEntity { + case .some(let signingEntity): + // Check signer(s) of other version(s) + if let otherSigner = signerVersions.keys.filter({ $0 != signingEntity }).first { + // There is a different signer + // TODO: This could indicate a legitimate change in package ownership + self.handleSigningEntityChanged( + package: package, + latest: signingEntity, + existing: otherSigner, + observabilityScope: observabilityScope + ) { result in + completion(result.tryMap { false }) + } + } else { + // Package doesn't have any other signer besides the given one, which is good. + completion(.success(true)) + } + case .none: + let versionSigners = signerVersions.versionSigners + let sortedSignedVersions = Array(versionSigners.keys).sorted(by: <) + for signedVersion in sortedSignedVersions { + // If the given version is semantically newer than any signed version, + // then it must be signed. (i.e., when a package starts being signed + // at a version, then all future versions must be signed.) + // TODO: we might want to allow package becoming unsigned + if version > signedVersion, let versionSigner = versionSigners[signedVersion] { + return self.handleSigningEntityChanged( + package: package, + latest: signingEntity, + existing: versionSigner, + observabilityScope: observabilityScope + ) { result in + completion(result.tryMap { false }) + } + } + } + // Assume this is an older version before package started getting signed + completion(.success(false)) + } + } + + private func writeToStorage( + package: PackageIdentity.RegistryIdentity, + version: Version, + signingEntity: SigningEntity, + observabilityScope: ObservabilityScope, + callbackQueue: DispatchQueue, + completion: @escaping (Result) -> Void + ) { + guard let signingEntityStorage = self.signingEntityStorage else { + return completion(.success(())) + } + + signingEntityStorage.put( + package: package.underlying, + version: version, + signingEntity: signingEntity, + observabilityScope: observabilityScope, + callbackQueue: callbackQueue + ) { result in + switch result { + case .success: + completion(.success(())) + case .failure(PackageSigningEntityStorageError.conflict(_, _, _, let existing)): + self.handleSigningEntityChanged( + package: package, + version: version, + latest: signingEntity, + existing: existing, + observabilityScope: observabilityScope, + completion: completion + ) + case .failure(let error): + completion(.failure(error)) + } + } + } + + private func handleSigningEntityChanged( + package: PackageIdentity.RegistryIdentity, + version: Version, + latest: SigningEntity?, + existing: SigningEntity, + observabilityScope: ObservabilityScope, + completion: @escaping (Result) -> Void + ) { + switch self.signingEntityCheckingMode { + case .strict: + completion(.failure(RegistryError.signingEntityForReleaseHasChanged( + package: package.underlying, + version: version, + latest: latest, + previous: existing + ))) + case .warn: + observabilityScope + .emit( + warning: "The signing entity \(String(describing: latest)) for '\(package)@\(version)' does not match previously recorded value \(existing)" + ) + completion(.success(())) + } + } -import Foundation + private func handleSigningEntityChanged( + package: PackageIdentity.RegistryIdentity, + latest: SigningEntity?, + existing: SigningEntity, + observabilityScope: ObservabilityScope, + completion: @escaping (Result) -> Void + ) { + switch self.signingEntityCheckingMode { + case .strict: + completion(.failure( + RegistryError + .signingEntityForPackageHasChanged(package: package.underlying, latest: latest, previous: existing) + )) + case .warn: + observabilityScope + .emit( + warning: "The signing entity \(String(describing: latest)) for '\(package)' does not match previously recorded value \(existing)" + ) + completion(.success(())) + } + } +} diff --git a/Sources/PackageSigning/CMakeLists.txt b/Sources/PackageSigning/CMakeLists.txt index 43a3c2c6dbe..ee7fb30b6eb 100644 --- a/Sources/PackageSigning/CMakeLists.txt +++ b/Sources/PackageSigning/CMakeLists.txt @@ -16,6 +16,7 @@ target_link_libraries(PackageSigning PUBLIC $<$>:dispatch> $<$>:Foundation> Basics + Crypto PackageModel TSCBasic TSCUtility) diff --git a/Sources/PackageSigning/SignatureProvider.swift b/Sources/PackageSigning/SignatureProvider.swift index c6dcdf3b17c..2a04771e560 100644 --- a/Sources/PackageSigning/SignatureProvider.swift +++ b/Sources/PackageSigning/SignatureProvider.swift @@ -74,7 +74,7 @@ public enum SignatureStatus: Equatable { case valid(SigningEntity) case invalid(String) case certificateInvalid(String) - case certificateNotTrusted + case certificateNotTrusted // TODO: include signer details } extension Certificate { diff --git a/Sources/PackageSigning/SigningEntity/FilePackageSigningEntityStorage.swift b/Sources/PackageSigning/SigningEntity/FilePackageSigningEntityStorage.swift index f27f2428656..eed6e7a26e4 100644 --- a/Sources/PackageSigning/SigningEntity/FilePackageSigningEntityStorage.swift +++ b/Sources/PackageSigning/SigningEntity/FilePackageSigningEntityStorage.swift @@ -161,14 +161,3 @@ extension PackageIdentity { "\(self.description).json" } } - -extension Dictionary where Key == SigningEntity, Value == Set { - public func signingEntity(of version: Version) -> SigningEntity? { - for (signingEntity, versions) in self { - if versions.contains(version) { - return signingEntity - } - } - return nil - } -} diff --git a/Sources/PackageSigning/SigningEntity/PackageSigningEntityStorage.swift b/Sources/PackageSigning/SigningEntity/PackageSigningEntityStorage.swift index aa9b4234860..775000d94e4 100644 --- a/Sources/PackageSigning/SigningEntity/PackageSigningEntityStorage.swift +++ b/Sources/PackageSigning/SigningEntity/PackageSigningEntityStorage.swift @@ -36,6 +36,27 @@ public protocol PackageSigningEntityStorage { ) } +extension Dictionary where Key == SigningEntity, Value == Set { + public func signingEntity(of version: Version) -> SigningEntity? { + for (signingEntity, versions) in self { + if versions.contains(version) { + return signingEntity + } + } + return nil + } + + public var versionSigners: [Version: SigningEntity] { + var versionSigners = [Version: SigningEntity]() + for (signingEntity, versions) in self { + versions.forEach { version in + versionSigners[version] = signingEntity + } + } + return versionSigners + } +} + public enum PackageSigningEntityStorageError: Error, Equatable, CustomStringConvertible { case conflict(package: PackageIdentity, version: Version, given: SigningEntity, existing: SigningEntity) From e1ae8e35b1d658c9c19eb90a74449b3b8fa2d98d Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Tue, 28 Feb 2023 11:44:37 -0800 Subject: [PATCH 10/20] SigningEntity TOFU requires type --- Sources/PackageRegistry/SigningEntityTOFU.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/PackageRegistry/SigningEntityTOFU.swift b/Sources/PackageRegistry/SigningEntityTOFU.swift index c37b012a255..4c5f6796903 100644 --- a/Sources/PackageRegistry/SigningEntityTOFU.swift +++ b/Sources/PackageRegistry/SigningEntityTOFU.swift @@ -58,7 +58,8 @@ struct PackageSigningEntityTOFU { ) { isOKResult in switch isOKResult { case .success(let shouldWrite): - guard shouldWrite, let signingEntity = signingEntity else { + // We only use certain type(s) of signing entity for TOFU + guard shouldWrite, let signingEntity = signingEntity, signingEntity.type != nil else { return completion(.success(())) } self.writeToStorage( From 2e378454b99caf9895e9f88a9727fec3c71c80c4 Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Tue, 28 Feb 2023 17:42:33 -0800 Subject: [PATCH 11/20] Add PackageSigningEntityTOFUTests --- Sources/PackageRegistry/RegistryClient.swift | 30 +- .../PackageRegistry/SigningEntityTOFU.swift | 61 +- .../PackageSigningEntityTOFUTests.swift | 759 ++++++++++++++++++ 3 files changed, 818 insertions(+), 32 deletions(-) create mode 100644 Tests/PackageRegistryTests/PackageSigningEntityTOFUTests.swift diff --git a/Sources/PackageRegistry/RegistryClient.swift b/Sources/PackageRegistry/RegistryClient.swift index 0450994c38b..7aeee747b9a 100644 --- a/Sources/PackageRegistry/RegistryClient.swift +++ b/Sources/PackageRegistry/RegistryClient.swift @@ -41,7 +41,7 @@ public final class RegistryClient: Cancellable { URL, (status: Result, expires: DispatchTime) >() - + private let metadataCache = ThreadSafeKeyValueStore< MetadataCacheKey, (metadata: Serialization.VersionMetadata, expires: DispatchTime) @@ -339,7 +339,7 @@ public final class RegistryClient: Cancellable { completion: @escaping (Result) -> Void ) { let completion = self.makeAsync(completion, on: callbackQueue) - + let cacheKey = MetadataCacheKey(registry: registry, package: package) if let cached = self.metadataCache[cacheKey], cached.expires < .now() { return completion(.success(cached.metadata)) @@ -1195,7 +1195,7 @@ public final class RegistryClient: Cancellable { options.authorizationProvider = self.authorizationProvider return options } - + private struct MetadataCacheKey: Hashable { let registry: Registry let package: PackageIdentity.RegistryIdentity @@ -1236,7 +1236,12 @@ public enum RegistryError: Error, CustomStringConvertible { case packageVersionNotFound case sourceArchiveNotSigned(registry: Registry, package: PackageIdentity, version: Version) case failedLoadingSignature - case failedRetrievingSourceArchiveSignature(registry: Registry, package: PackageIdentity, version: Version, error: Error) + case failedRetrievingSourceArchiveSignature( + registry: Registry, + package: PackageIdentity, + version: Version, + error: Error + ) case missingConfiguration(details: String) case missingSignatureFormat case unknownSignatureFormat(String) @@ -1244,8 +1249,13 @@ public enum RegistryError: Error, CustomStringConvertible { case invalidSigningCertificate(reason: String) case signerNotTrusted case failedToValidateSignature(Error) - case signingEntityForReleaseHasChanged(package: PackageIdentity, version: Version, latest: SigningEntity?, previous: SigningEntity) - case signingEntityForPackageHasChanged(package: PackageIdentity, latest: SigningEntity?, previous: SigningEntity) + case signingEntityForReleaseChanged( + package: PackageIdentity, + version: Version, + latest: SigningEntity?, + previous: SigningEntity + ) + case signingEntityForPackageChanged(package: PackageIdentity, latest: SigningEntity?, previous: SigningEntity) public var description: String { switch self { @@ -1335,9 +1345,9 @@ public enum RegistryError: Error, CustomStringConvertible { return "The signer is not trusted" case .failedToValidateSignature(let error): return "Failed to validate signature: \(error)" - case .signingEntityForReleaseHasChanged(let package, let version, let latest, let previous): + case .signingEntityForReleaseChanged(let package, let version, let latest, let previous): return "The signing entity '\(String(describing: latest))' for '\(package)@\(version)' is different from the previously recorded value '\(previous)'" - case .signingEntityForPackageHasChanged(let package, let latest, let previous): + case .signingEntityForPackageChanged(let package, let latest, let previous): return "The signing entity '\(String(describing: latest))' for '\(package)' is different from the previously recorded value '\(previous)'" } } @@ -1667,7 +1677,7 @@ extension RegistryClient { public let version: String public let resources: [Resource] public let metadata: AdditionalMetadata? - + var sourceArchive: Resource? { self.resources.first(where: { $0.name == "source-archive" }) } @@ -1697,7 +1707,7 @@ extension RegistryClient { self.signing = signing } } - + public struct Signing: Codable { public let signatureBase64Encoded: String public let signatureFormat: String diff --git a/Sources/PackageRegistry/SigningEntityTOFU.swift b/Sources/PackageRegistry/SigningEntityTOFU.swift index 4c5f6796903..58f89bdea16 100644 --- a/Sources/PackageRegistry/SigningEntityTOFU.swift +++ b/Sources/PackageRegistry/SigningEntityTOFU.swift @@ -49,17 +49,17 @@ struct PackageSigningEntityTOFU { ) { result in switch result { case .success(let signerVersions): - self.isSigningEntityOK( + self.validateSigningEntity( package: package, version: version, signingEntity: signingEntity, signerVersions: signerVersions, observabilityScope: observabilityScope - ) { isOKResult in - switch isOKResult { + ) { validateResult in + switch validateResult { case .success(let shouldWrite): // We only use certain type(s) of signing entity for TOFU - guard shouldWrite, let signingEntity = signingEntity, signingEntity.type != nil else { + guard shouldWrite, let signingEntity = signingEntity, signingEntity.isRecognized else { return completion(.success(())) } self.writeToStorage( @@ -81,7 +81,7 @@ struct PackageSigningEntityTOFU { } } - private func isSigningEntityOK( + private func validateSigningEntity( package: PackageIdentity.RegistryIdentity, version: Version, signingEntity: SigningEntity?, @@ -91,7 +91,7 @@ struct PackageSigningEntityTOFU { ) { // Package is never signed. // If signingEntity is nil, it means package remains unsigned, which is OK. (none -> none) - // Otherwise, we are now assigning a signing entity, which is also OK. (none -> some) + // Otherwise, package has gained a signing entity, which is also OK. (none -> some) if signerVersions.isEmpty { return completion(.success(true)) } @@ -102,8 +102,10 @@ struct PackageSigningEntityTOFU { // e.g., change of package ownership, package author decides to stop signing releases, etc. // Instead of failing, we should allow and prompt user to add/replace/remove signing entity. + // We recorded the version's signer previously if let signerForVersion = signerVersions.signingEntity(of: version) { - // We recorded a different signer for the given version before. This is NOT OK. + // The given signer is different + // TODO: This could indicate a legitimate change in package ownership guard signerForVersion == signingEntity else { return self.handleSigningEntityChanged( package: package, @@ -115,11 +117,12 @@ struct PackageSigningEntityTOFU { completion(result.tryMap { false }) } } - // Signer remains the same, which could be nil, for the version. - completion(.success(false)) + // Signer remains the same for the version + return completion(.success(false)) } switch signingEntity { + // Is the package changing from one signer to another? case .some(let signingEntity): // Check signer(s) of other version(s) if let otherSigner = signerVersions.keys.filter({ $0 != signingEntity }).first { @@ -137,15 +140,29 @@ struct PackageSigningEntityTOFU { // Package doesn't have any other signer besides the given one, which is good. completion(.success(true)) } + // Or is the package going from having a signer to .none? case .none: let versionSigners = signerVersions.versionSigners - let sortedSignedVersions = Array(versionSigners.keys).sorted(by: <) - for signedVersion in sortedSignedVersions { - // If the given version is semantically newer than any signed version, - // then it must be signed. (i.e., when a package starts being signed - // at a version, then all future versions must be signed.) - // TODO: we might want to allow package becoming unsigned - if version > signedVersion, let versionSigner = versionSigners[signedVersion] { + // If the given version is semantically newer than any signed version, + // then it must be signed. (i.e., when a package starts being signed + // at a version, then all future versions must be signed.) + // TODO: We might want to allow package becoming unsigned + // + // Here we try to handle the scenario where there is more than + // one major version branch, and signing didn't start from the beginning + // for both of them. For example, suppose a project has 1.x and 2.x active + // major versions, and signing starts at 1.2.0 and 2.2.0. The first version + // that SwiftPM downloads is 1.5.0, which is signed and signer gets recorded. + // - When unsigned v1.1.0 is downloaded, we don't fail because it's + // an older version (i.e., < 1.5.0) and we allow it to be unsigned. + // - When unsigned v1.6.0 is downloaded, we fail because it's + // a newer version (i.e., < 1.5.0) and we assume it to be signed. + // - When unsigned v2.0.0 is downloaded, we don't fail because we haven't + // seen a signed 2.x release yet, so we assume 2.x releases are not signed. + let olderSignedVersions = versionSigners.keys.filter { $0.major == version.major && $0 < version } + .sorted(by: >) + for signedVersion in olderSignedVersions { + if let versionSigner = versionSigners[signedVersion] { return self.handleSigningEntityChanged( package: package, latest: signingEntity, @@ -156,7 +173,7 @@ struct PackageSigningEntityTOFU { } } } - // Assume this is an older version before package started getting signed + // Assume the given version is an older version before package started getting signed completion(.success(false)) } } @@ -208,7 +225,7 @@ struct PackageSigningEntityTOFU { ) { switch self.signingEntityCheckingMode { case .strict: - completion(.failure(RegistryError.signingEntityForReleaseHasChanged( + completion(.failure(RegistryError.signingEntityForReleaseChanged( package: package.underlying, version: version, latest: latest, @@ -232,10 +249,10 @@ struct PackageSigningEntityTOFU { ) { switch self.signingEntityCheckingMode { case .strict: - completion(.failure( - RegistryError - .signingEntityForPackageHasChanged(package: package.underlying, latest: latest, previous: existing) - )) + completion(.failure(RegistryError.signingEntityForPackageChanged( + package: package.underlying, + latest: latest, previous: existing + ))) case .warn: observabilityScope .emit( diff --git a/Tests/PackageRegistryTests/PackageSigningEntityTOFUTests.swift b/Tests/PackageRegistryTests/PackageSigningEntityTOFUTests.swift new file mode 100644 index 00000000000..140bce73230 --- /dev/null +++ b/Tests/PackageRegistryTests/PackageSigningEntityTOFUTests.swift @@ -0,0 +1,759 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2023 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 the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Basics +import PackageModel +@testable import PackageRegistry +import PackageSigning +import SPMTestSupport +import TSCBasic +import XCTest + +import struct TSCUtility.Version + +final class PackageSigningEntityTOFUTests: XCTestCase { + func testSigningEntitySeenForTheFirstTime() throws { + let package = PackageIdentity.plain("mona.LinkedList").registry! + let version = Version("1.1.1") + let signingEntity = SigningEntity( + type: .adp, // This makes isRecognized == true + name: "J. Appleseed", + organizationalUnit: nil, + organization: nil + ) + + let signingEntityStorage = MockPackageSigningEntityStorage() + let signingEntityCheckingMode = SigningEntityCheckingMode.strict + + let tofu = PackageSigningEntityTOFU( + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode + ) + + // Package doesn't have any recorded signer. + // It should be ok to assign one. + XCTAssertNoThrow( + try tofu.validate( + package: package, + version: version, + signingEntity: signingEntity + ) + ) + + // `signingEntity` meets requirement to be used for TOFU + // (i.e., its type is non-nil), so it should be saved to storage. + let signedVersions = try tsc_await { callback in + signingEntityStorage.get( + package: package.underlying, + observabilityScope: ObservabilitySystem.NOOP, + callbackQueue: .sharedConcurrent, + callback: callback + ) + } + XCTAssertEqual(signedVersions.count, 1) + XCTAssertEqual(signedVersions[signingEntity], [version]) + } + + func testNilSigningEntityShouldNotBeSaved() throws { + let package = PackageIdentity.plain("mona.LinkedList").registry! + let version = Version("1.1.1") + + let signingEntityStorage = MockPackageSigningEntityStorage() + let signingEntityCheckingMode = SigningEntityCheckingMode.strict + + let tofu = PackageSigningEntityTOFU( + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode + ) + + // Package doesn't have any recorded signer. + // It should be ok to continue not to have one. + XCTAssertNoThrow( + try tofu.validate( + package: package, + version: version, + signingEntity: .none + ) + ) + + // `signingEntity` is nil, so it should not be saved to storage. + let signedVersions = try tsc_await { callback in + signingEntityStorage.get( + package: package.underlying, + observabilityScope: ObservabilitySystem.NOOP, + callbackQueue: .sharedConcurrent, + callback: callback + ) + } + XCTAssertTrue(signedVersions.isEmpty) + } + + func testUnrecognizedSigningEntityShouldNotBeSaved() throws { + let package = PackageIdentity.plain("mona.LinkedList").registry! + let version = Version("1.1.1") + let signingEntity = SigningEntity( + type: nil, // This makes isRecognized == false + name: "J. Appleseed", + organizationalUnit: nil, + organization: nil + ) + + let signingEntityStorage = MockPackageSigningEntityStorage() + let signingEntityCheckingMode = SigningEntityCheckingMode.strict + + let tofu = PackageSigningEntityTOFU( + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode + ) + + // Package doesn't have any recorded signer. + // It should be ok to continue not to have one. + XCTAssertNoThrow( + try tofu.validate( + package: package, + version: version, + signingEntity: signingEntity + ) + ) + + // `signingEntity` is not recognized, so it should not be saved to storage. + let signedVersions = try tsc_await { callback in + signingEntityStorage.get( + package: package.underlying, + observabilityScope: ObservabilitySystem.NOOP, + callbackQueue: .sharedConcurrent, + callback: callback + ) + } + XCTAssertTrue(signedVersions.isEmpty) + } + + func testSigningEntityMatchesStorageForSameVersion() throws { + let package = PackageIdentity.plain("mona.LinkedList").registry! + let version = Version("1.1.1") + let signingEntity = SigningEntity( + type: .adp, + name: "J. Appleseed", + organizationalUnit: nil, + organization: nil + ) + + let signingEntityStorage = MockPackageSigningEntityStorage( + [package.underlying: [signingEntity: [version]]] + ) + let signingEntityCheckingMode = SigningEntityCheckingMode.strict + + let tofu = PackageSigningEntityTOFU( + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode + ) + + // Storage has "J. Appleseed" as signer for package version. + // Signer remaining the same should be ok. + XCTAssertNoThrow( + try tofu.validate( + package: package, + version: version, + signingEntity: signingEntity + ) + ) + } + + func testSigningEntityDoesNotMatchStorageForSameVersion_strictMode() throws { + let package = PackageIdentity.plain("mona.LinkedList").registry! + let version = Version("1.1.1") + let signingEntity = SigningEntity( + type: .adp, + name: "J. Appleseed", + organizationalUnit: nil, + organization: nil + ) + let existingSigner = SigningEntity( + type: .adp, + name: "J. Smith", + organizationalUnit: nil, + organization: nil + ) + + let signingEntityStorage = MockPackageSigningEntityStorage( + [package.underlying: [existingSigner: [version]]] + ) + let signingEntityCheckingMode = SigningEntityCheckingMode.strict // intended for this test; don't change + + let tofu = PackageSigningEntityTOFU( + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode + ) + + // Storage has "J. Smith" as signer for package version. + // The given signer "J. Appleseed" is different so it should fail. + XCTAssertThrowsError( + try tofu.validate( + package: package, + version: version, + signingEntity: signingEntity + ) + ) { error in + guard case RegistryError.signingEntityForReleaseChanged(_, _, _, let previous) = error else { + return XCTFail("Expected RegistryError.signingEntityForReleaseChanged, got '\(error)'") + } + XCTAssertEqual(previous, existingSigner) + } + + // Storage should not be updated + let signedVersions = try tsc_await { callback in + signingEntityStorage.get( + package: package.underlying, + observabilityScope: ObservabilitySystem.NOOP, + callbackQueue: .sharedConcurrent, + callback: callback + ) + } + XCTAssertEqual(signedVersions.count, 1) + XCTAssertEqual(signedVersions[existingSigner], [version]) + } + + func testSigningEntityDoesNotMatchStorageForSameVersion_warnMode() throws { + let package = PackageIdentity.plain("mona.LinkedList").registry! + let version = Version("1.1.1") + let signingEntity = SigningEntity( + type: .adp, + name: "J. Appleseed", + organizationalUnit: nil, + organization: nil + ) + let existingSigner = SigningEntity( + type: .adp, + name: "J. Smith", + organizationalUnit: nil, + organization: nil + ) + + let signingEntityStorage = MockPackageSigningEntityStorage( + [package.underlying: [existingSigner: [version]]] + ) + let signingEntityCheckingMode = SigningEntityCheckingMode.warn // intended for this test; don't change + + let tofu = PackageSigningEntityTOFU( + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode + ) + + let observability = ObservabilitySystem.makeForTesting() + + // Storage has "J. Smith" as signer for package version. + // The given signer "J. Appleseed" is different, but because + // of .warn mode, no error is thrown. + XCTAssertNoThrow( + try tofu.validate( + package: package, + version: version, + signingEntity: signingEntity, + observabilityScope: observability.topScope + ) + ) + + // But there should be a warning + testDiagnostics(observability.diagnostics) { result in + result.check(diagnostic: .contains("does not match previously recorded value"), severity: .warning) + } + + // Storage should not be updated + let signedVersions = try tsc_await { callback in + signingEntityStorage.get( + package: package.underlying, + observabilityScope: ObservabilitySystem.NOOP, + callbackQueue: .sharedConcurrent, + callback: callback + ) + } + XCTAssertEqual(signedVersions.count, 1) + XCTAssertEqual(signedVersions[existingSigner], [version]) + } + + func testSigningEntityMatchesStorageForDifferentVersion() throws { + let package = PackageIdentity.plain("mona.LinkedList").registry! + let version = Version("1.1.1") + let existingVersion = Version("2.0.0") + let signingEntity = SigningEntity( + type: .adp, + name: "J. Appleseed", + organizationalUnit: nil, + organization: nil + ) + + let signingEntityStorage = MockPackageSigningEntityStorage( + [package.underlying: [signingEntity: [existingVersion]]] + ) + let signingEntityCheckingMode = SigningEntityCheckingMode.strict + + let tofu = PackageSigningEntityTOFU( + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode + ) + + // Storage has "J. Appleseed" as signer for package. + // Signer remaining the same should be ok. + XCTAssertNoThrow( + try tofu.validate( + package: package, + version: version, + signingEntity: signingEntity + ) + ) + + // Storage should be updated with version 1.1.1 added + let signedVersions = try tsc_await { callback in + signingEntityStorage.get( + package: package.underlying, + observabilityScope: ObservabilitySystem.NOOP, + callbackQueue: .sharedConcurrent, + callback: callback + ) + } + XCTAssertEqual(signedVersions.count, 1) + XCTAssertEqual(signedVersions[signingEntity], [existingVersion, version]) + } + + func testSigningEntityDoesNotMatchStorageForDifferentVersion_strictMode() throws { + let package = PackageIdentity.plain("mona.LinkedList").registry! + let version = Version("1.1.1") + let existingVersion = Version("2.0.0") + let signingEntity = SigningEntity( + type: .adp, + name: "J. Appleseed", + organizationalUnit: nil, + organization: nil + ) + let existingSigner = SigningEntity( + type: .adp, + name: "J. Smith", + organizationalUnit: nil, + organization: nil + ) + + let signingEntityStorage = MockPackageSigningEntityStorage( + [package.underlying: [existingSigner: [existingVersion]]] + ) + let signingEntityCheckingMode = SigningEntityCheckingMode.strict // intended for this test; don't change + + let tofu = PackageSigningEntityTOFU( + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode + ) + + // Storage has "J. Smith" as signer for package. + // The given signer "J. Appleseed" is different so it should fail. + XCTAssertThrowsError( + try tofu.validate( + package: package, + version: version, + signingEntity: signingEntity + ) + ) { error in + guard case RegistryError.signingEntityForPackageChanged(_, _, let previous) = error else { + return XCTFail("Expected RegistryError.signingEntityForPackageChanged, got '\(error)'") + } + XCTAssertEqual(previous, existingSigner) + } + + // Storage should not be updated + let signedVersions = try tsc_await { callback in + signingEntityStorage.get( + package: package.underlying, + observabilityScope: ObservabilitySystem.NOOP, + callbackQueue: .sharedConcurrent, + callback: callback + ) + } + XCTAssertEqual(signedVersions.count, 1) + XCTAssertEqual(signedVersions[existingSigner], [existingVersion]) + } + + func testSigningEntityDoesNotMatchStorageForDifferentVersion_warnMode() throws { + let package = PackageIdentity.plain("mona.LinkedList").registry! + let version = Version("1.1.1") + let existingVersion = Version("2.0.0") + let signingEntity = SigningEntity( + type: .adp, + name: "J. Appleseed", + organizationalUnit: nil, + organization: nil + ) + let existingSigner = SigningEntity( + type: .adp, + name: "J. Smith", + organizationalUnit: nil, + organization: nil + ) + + let signingEntityStorage = MockPackageSigningEntityStorage( + [package.underlying: [existingSigner: [existingVersion]]] + ) + let signingEntityCheckingMode = SigningEntityCheckingMode.warn // intended for this test; don't change + + let tofu = PackageSigningEntityTOFU( + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode + ) + + let observability = ObservabilitySystem.makeForTesting() + + // Storage has "J. Smith" as signer for package. + // The given signer "J. Appleseed" is different, but because + // of .warn mode, no error is thrown. + XCTAssertNoThrow( + try tofu.validate( + package: package, + version: version, + signingEntity: signingEntity, + observabilityScope: observability.topScope + ) + ) + + // But there should be a warning + testDiagnostics(observability.diagnostics) { result in + result.check(diagnostic: .contains("does not match previously recorded value"), severity: .warning) + } + + // Storage should not be updated + let signedVersions = try tsc_await { callback in + signingEntityStorage.get( + package: package.underlying, + observabilityScope: ObservabilitySystem.NOOP, + callbackQueue: .sharedConcurrent, + callback: callback + ) + } + XCTAssertEqual(signedVersions.count, 1) + XCTAssertEqual(signedVersions[existingSigner], [existingVersion]) + } + + func testNilSigningEntityWhenStorageHasNewerSignedVersions() throws { + let package = PackageIdentity.plain("mona.LinkedList").registry! + let version = Version("1.1.1") + let existingVersions = Set([Version("1.5.0"), Version("2.0.0")]) + let existingSigner = SigningEntity( + type: .adp, + name: "J. Smith", + organizationalUnit: nil, + organization: nil + ) + + let signingEntityStorage = MockPackageSigningEntityStorage( + [package.underlying: [existingSigner: existingVersions]] + ) + let signingEntityCheckingMode = SigningEntityCheckingMode.strict + + let tofu = PackageSigningEntityTOFU( + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode + ) + + // Storage has versions 1.5.0 and 2.0.0 signed. The given version 1.1.1 is + // "older" than both, and we allow nil signer in this case, assuming + // this is before package started being signed. + XCTAssertNoThrow( + try tofu.validate( + package: package, + version: version, + signingEntity: .none + ) + ) + + // Storage should not be updated + let signedVersions = try tsc_await { callback in + signingEntityStorage.get( + package: package.underlying, + observabilityScope: ObservabilitySystem.NOOP, + callbackQueue: .sharedConcurrent, + callback: callback + ) + } + XCTAssertEqual(signedVersions.count, 1) + XCTAssertEqual(signedVersions[existingSigner], existingVersions) + } + + func testNilSigningEntityWhenStorageHasOlderSignedVersions_strictMode() throws { + let package = PackageIdentity.plain("mona.LinkedList").registry! + let version = Version("1.6.1") + let existingVersions = Set([Version("1.5.0"), Version("2.0.0")]) + let existingSigner = SigningEntity( + type: .adp, + name: "J. Smith", + organizationalUnit: nil, + organization: nil + ) + + let signingEntityStorage = MockPackageSigningEntityStorage( + [package.underlying: [existingSigner: existingVersions]] + ) + let signingEntityCheckingMode = SigningEntityCheckingMode.strict // intended for this test; don't change + + let tofu = PackageSigningEntityTOFU( + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode + ) + + // Storage has versions 1.5.0 and 2.0.0 signed. The given version 1.6.1 is + // "newer" than 1.5.0, which we don't allow, because we assume from 1.5.0 + // onwards all versions are signed. + XCTAssertThrowsError( + try tofu.validate( + package: package, + version: version, + signingEntity: .none + ) + ) { error in + guard case RegistryError.signingEntityForPackageChanged(_, let latest, let previous) = error else { + return XCTFail("Expected RegistryError.signingEntityForPackageChanged, got '\(error)'") + } + XCTAssertNil(latest) + XCTAssertEqual(previous, existingSigner) + } + + // Storage should not be updated + let signedVersions = try tsc_await { callback in + signingEntityStorage.get( + package: package.underlying, + observabilityScope: ObservabilitySystem.NOOP, + callbackQueue: .sharedConcurrent, + callback: callback + ) + } + XCTAssertEqual(signedVersions.count, 1) + XCTAssertEqual(signedVersions[existingSigner], existingVersions) + } + + func testNilSigningEntityWhenStorageHasOlderSignedVersions_warnMode() throws { + let package = PackageIdentity.plain("mona.LinkedList").registry! + let version = Version("1.6.1") + let existingVersions = Set([Version("1.5.0"), Version("2.0.0")]) + let existingSigner = SigningEntity( + type: .adp, + name: "J. Smith", + organizationalUnit: nil, + organization: nil + ) + + let signingEntityStorage = MockPackageSigningEntityStorage( + [package.underlying: [existingSigner: existingVersions]] + ) + let signingEntityCheckingMode = SigningEntityCheckingMode.warn // intended for this test; don't change + + let tofu = PackageSigningEntityTOFU( + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode + ) + + let observability = ObservabilitySystem.makeForTesting() + + // Storage has versions 1.5.0 and 2.0.0 signed. The given version 1.6.1 is + // "newer" than 1.5.0, which we don't allow, because we assume from 1.5.0 + // onwards all versions are signed. However, because of .warn mode, + // no error is thrown. + XCTAssertNoThrow( + try tofu.validate( + package: package, + version: version, + signingEntity: .none, + observabilityScope: observability.topScope + ) + ) + + // But there should be a warning + testDiagnostics(observability.diagnostics) { result in + result.check(diagnostic: .contains("does not match previously recorded value"), severity: .warning) + } + + // Storage should not be updated + let signedVersions = try tsc_await { callback in + signingEntityStorage.get( + package: package.underlying, + observabilityScope: ObservabilitySystem.NOOP, + callbackQueue: .sharedConcurrent, + callback: callback + ) + } + XCTAssertEqual(signedVersions.count, 1) + XCTAssertEqual(signedVersions[existingSigner], existingVersions) + } + + func testNilSigningEntityWhenStorageHasOlderSignedVersionsInDifferentMajorVersion() throws { + let package = PackageIdentity.plain("mona.LinkedList").registry! + let version = Version("2.0.0") + let existingVersions = Set([Version("1.5.0"), Version("3.0.0")]) + let existingSigner = SigningEntity( + type: .adp, + name: "J. Smith", + organizationalUnit: nil, + organization: nil + ) + + let signingEntityStorage = MockPackageSigningEntityStorage( + [package.underlying: [existingSigner: existingVersions]] + ) + let signingEntityCheckingMode = SigningEntityCheckingMode.strict + + let tofu = PackageSigningEntityTOFU( + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode + ) + + // Storage has versions 1.5.0 and 3.0.0 signed. The given version 2.0.0 is + // "newer" than 1.5.0, but in a different major version (i.e., 1.x vs. 2.x). + // We allow this with the assumption that package signing might not have + // begun until a later 2.x version, so until we encounter a signed 2.x version, + // we assume none of them is signed. + XCTAssertNoThrow( + try tofu.validate( + package: package, + version: version, + signingEntity: .none + ) + ) + + // Storage should not be updated + let signedVersions = try tsc_await { callback in + signingEntityStorage.get( + package: package.underlying, + observabilityScope: ObservabilitySystem.NOOP, + callbackQueue: .sharedConcurrent, + callback: callback + ) + } + XCTAssertEqual(signedVersions.count, 1) + XCTAssertEqual(signedVersions[existingSigner], existingVersions) + } + + func testWriteConflictsWithStorage_strictMode() throws { + let package = PackageIdentity.plain("mona.LinkedList").registry! + let version = Version("1.1.1") + let signingEntity = SigningEntity( + type: .adp, + name: "J. Appleseed", + organizationalUnit: nil, + organization: nil + ) + + let signingEntityStorage = WriteConflictSigningEntityStorage() + let signingEntityCheckingMode = SigningEntityCheckingMode.strict // intended for this test; don't change + + let tofu = PackageSigningEntityTOFU( + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode + ) + + // This triggers a storage write conflict + XCTAssertThrowsError( + try tofu.validate( + package: package, + version: version, + signingEntity: signingEntity + ) + ) { error in + guard case RegistryError.signingEntityForReleaseChanged = error else { + return XCTFail("Expected RegistryError.signingEntityForReleaseChanged, got '\(error)'") + } + } + } + + func testWriteConflictsWithStorage_warnMode() throws { + let package = PackageIdentity.plain("mona.LinkedList").registry! + let version = Version("1.1.1") + let signingEntity = SigningEntity( + type: .adp, + name: "J. Appleseed", + organizationalUnit: nil, + organization: nil + ) + + let signingEntityStorage = WriteConflictSigningEntityStorage() + let signingEntityCheckingMode = SigningEntityCheckingMode.warn // intended for this test; don't change + + let tofu = PackageSigningEntityTOFU( + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode + ) + + let observability = ObservabilitySystem.makeForTesting() + + // This triggers a storage write conflict, but + // because of .warn mode, no error is thrown. + XCTAssertNoThrow( + try tofu.validate( + package: package, + version: version, + signingEntity: signingEntity, + observabilityScope: observability.topScope + ) + ) + + // But there should be a warning + testDiagnostics(observability.diagnostics) { result in + result.check(diagnostic: .contains("does not match previously recorded value"), severity: .warning) + } + } +} + +extension PackageSigningEntityTOFU { + fileprivate func validate( + package: PackageIdentity.RegistryIdentity, + version: Version, + signingEntity: SigningEntity?, + observabilityScope: ObservabilityScope? = nil + ) throws { + try tsc_await { + self.validate( + package: package, + version: version, + signingEntity: signingEntity, + observabilityScope: observabilityScope ?? ObservabilitySystem.NOOP, + callbackQueue: .sharedConcurrent, + completion: $0 + ) + } + } +} + +private class WriteConflictSigningEntityStorage: PackageSigningEntityStorage { + func get( + package: PackageIdentity, + observabilityScope: ObservabilityScope, + callbackQueue: DispatchQueue, + callback: @escaping (Result<[SigningEntity: Set], Error>) -> Void + ) { + callback(.success([:])) + } + + func put( + package: PackageIdentity, + version: Version, + signingEntity: SigningEntity, + observabilityScope: ObservabilityScope, + callbackQueue: DispatchQueue, + callback: @escaping (Result) -> Void + ) { + let existing = SigningEntity( + type: nil, + name: "xxx-\(signingEntity.name ?? "")", + organizationalUnit: nil, + organization: nil + ) + callback(.failure(PackageSigningEntityStorageError.conflict( + package: package, + version: version, + given: signingEntity, + existing: existing + ))) + } +} From 66195760de73cf3291806506c7f9dcd3cba76097 Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Tue, 28 Feb 2023 17:47:28 -0800 Subject: [PATCH 12/20] Work around 'Task' not found error in CI --- Sources/PackageRegistry/SignatureValidation.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/PackageRegistry/SignatureValidation.swift b/Sources/PackageRegistry/SignatureValidation.swift index 17553ec2fe7..f53aaaec4ed 100644 --- a/Sources/PackageRegistry/SignatureValidation.swift +++ b/Sources/PackageRegistry/SignatureValidation.swift @@ -144,6 +144,7 @@ struct SignatureValidation { observabilityScope: ObservabilityScope, completion: @escaping (Result) -> Void ) { + #if swift(>=5.5.2) Task { do { let signatureStatus = try await SignatureProvider.status( @@ -191,6 +192,7 @@ struct SignatureValidation { completion(.failure(RegistryError.failedToValidateSignature(error))) } } + #endif } private func getSourceArchiveSignature( From 992971c091d167bddadc24265bb8025ba606a0e5 Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Tue, 28 Feb 2023 21:33:53 -0800 Subject: [PATCH 13/20] Add SignatureValidationTests --- Sources/PackageRegistry/RegistryClient.swift | 13 +- .../RegistryConfiguration.swift | 18 ++ .../PackageRegistry/SignatureValidation.swift | 87 +++--- .../PackageVersionChecksumTOFUTests.swift | 65 +--- .../SignatureValidationTests.swift | 292 ++++++++++++++++++ 5 files changed, 371 insertions(+), 104 deletions(-) create mode 100644 Tests/PackageRegistryTests/SignatureValidationTests.swift diff --git a/Sources/PackageRegistry/RegistryClient.swift b/Sources/PackageRegistry/RegistryClient.swift index 7aeee747b9a..4d753b9b346 100644 --- a/Sources/PackageRegistry/RegistryClient.swift +++ b/Sources/PackageRegistry/RegistryClient.swift @@ -34,8 +34,8 @@ public final class RegistryClient: Cancellable { private let authorizationProvider: LegacyHTTPClientConfiguration.AuthorizationProvider? private let jsonDecoder: JSONDecoder - private var checksumTOFU: PackageVersionChecksumTOFU! - private let signingEntityTOFU: PackageSigningEntityTOFU + private(set) var checksumTOFU: PackageVersionChecksumTOFU! + private(set) var signatureValidation: SignatureValidation! private let availabilityCache = ThreadSafeKeyValueStore< URL, @@ -87,15 +87,16 @@ public final class RegistryClient: Cancellable { self.archiverProvider = customArchiverProvider ?? { fileSystem in ZipArchiver(fileSystem: fileSystem) } self.jsonDecoder = JSONDecoder.makeWithDefaults() - self.signingEntityTOFU = PackageSigningEntityTOFU( - signingEntityStorage: signingEntityStorage, - signingEntityCheckingMode: signingEntityCheckingMode - ) self.checksumTOFU = PackageVersionChecksumTOFU( fingerprintStorage: fingerprintStorage, fingerprintCheckingMode: fingerprintCheckingMode, registryClient: self ) + self.signatureValidation = SignatureValidation( + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode, + registryClient: self + ) } public var explicitlyConfigured: Bool { diff --git a/Sources/PackageRegistry/RegistryConfiguration.swift b/Sources/PackageRegistry/RegistryConfiguration.swift index b149dc14050..093150bb8c6 100644 --- a/Sources/PackageRegistry/RegistryConfiguration.swift +++ b/Sources/PackageRegistry/RegistryConfiguration.swift @@ -122,12 +122,30 @@ extension RegistryConfiguration { self.packageOverrides = [:] } + // for testing + init( + default: Global, + registryOverrides: [String: RegistryOverride] = [:], + scopeOverrides: [PackageIdentity.Scope: ScopePackageOverride] = [:], + packageOverrides: [PackageIdentity.RegistryIdentity: ScopePackageOverride] = [:] + ) { + self.default = `default` + self.registryOverrides = registryOverrides + self.scopeOverrides = scopeOverrides + self.packageOverrides = packageOverrides + } + public struct Global: Hashable, Codable { public var signing: Signing? public init() { self.signing = nil } + + // for testing + init(signing: Signing) { + self.signing = signing + } } public struct RegistryOverride: Hashable, Codable { diff --git a/Sources/PackageRegistry/SignatureValidation.swift b/Sources/PackageRegistry/SignatureValidation.swift index f53aaaec4ed..da8c81af949 100644 --- a/Sources/PackageRegistry/SignatureValidation.swift +++ b/Sources/PackageRegistry/SignatureValidation.swift @@ -20,19 +20,19 @@ import PackageSigning import struct TSCUtility.Version struct SignatureValidation { - private let registryClient: RegistryClient private let signingEntityTOFU: PackageSigningEntityTOFU + private let registryClient: RegistryClient init( - registryClient: RegistryClient, signingEntityStorage: PackageSigningEntityStorage?, - signingEntityCheckingMode: SigningEntityCheckingMode + signingEntityCheckingMode: SigningEntityCheckingMode, + registryClient: RegistryClient ) { - self.registryClient = registryClient self.signingEntityTOFU = PackageSigningEntityTOFU( signingEntityStorage: signingEntityStorage, signingEntityCheckingMode: signingEntityCheckingMode ) + self.registryClient = registryClient } func validate( @@ -58,6 +58,8 @@ struct SignatureValidation { ) { result in completion( result.tryMap { signingEntity in + // Always do signing entity TOFU check at the end, + // whether the package is signed or not. self.signingEntityTOFU.validate( package: package, version: version, @@ -124,10 +126,10 @@ struct SignatureValidation { } case .warn: observabilityScope.emit(warning: "\(error)") - completion(.success(nil)) + completion(.success(.none)) case .silentAllow: // Continue without logging - completion(.success(nil)) + completion(.success(.none)) } } } @@ -182,16 +184,18 @@ struct SignatureValidation { case .warn: // TODO: populate error with signer detail observabilityScope.emit(warning: "\(RegistryError.signerNotTrusted)") - completion(.success(nil)) + completion(.success(.none)) case .silentAllow: // Continue without logging - completion(.success(nil)) + completion(.success(.none)) } } } catch { completion(.failure(RegistryError.failedToValidateSignature(error))) } } + #else + completion(.success(.none)) #endif } @@ -212,43 +216,42 @@ struct SignatureValidation { observabilityScope: observabilityScope, callbackQueue: callbackQueue ) { result in - completion( - result.tryMap { metadata in - guard let sourceArchive = metadata.sourceArchive else { - throw RegistryError.missingSourceArchive - } - guard let signatureBase64Encoded = sourceArchive.signing?.signatureBase64Encoded else { - throw RegistryError.sourceArchiveNotSigned( - registry: registry, - package: package.underlying, - version: version - ) - } - guard let signatureData = Data(base64Encoded: signatureBase64Encoded) else { - throw RegistryError.failedLoadingSignature - } - guard let signatureFormatString = sourceArchive.signing?.signatureFormat else { - throw RegistryError.missingSignatureFormat - } - guard let signatureFormat = SignatureFormat(rawValue: signatureFormatString) else { - throw RegistryError.unknownSignatureFormat(signatureFormatString) - } - return (signatureData, signatureFormat) - }.mapError { error in - let actualError: Error - if case RegistryError.failedRetrievingReleaseInfo(_, _, _, let error) = error { - actualError = error - } else { - actualError = error - } - return RegistryError.failedRetrievingSourceArchiveSignature( + switch result { + case .success(let metadata): + guard let sourceArchive = metadata.sourceArchive else { + return completion(.failure(RegistryError.missingSourceArchive)) + } + guard let signatureBase64Encoded = sourceArchive.signing?.signatureBase64Encoded else { + return completion(.failure(RegistryError.sourceArchiveNotSigned( registry: registry, package: package.underlying, - version: version, - error: actualError - ) + version: version + ))) } - ) + guard let signatureData = Data(base64Encoded: signatureBase64Encoded) else { + return completion(.failure(RegistryError.failedLoadingSignature)) + } + guard let signatureFormatString = sourceArchive.signing?.signatureFormat else { + return completion(.failure(RegistryError.missingSignatureFormat)) + } + guard let signatureFormat = SignatureFormat(rawValue: signatureFormatString) else { + return completion(.failure(RegistryError.unknownSignatureFormat(signatureFormatString))) + } + completion(.success((signatureData, signatureFormat))) + case .failure(let error): + let actualError: Error + if case RegistryError.failedRetrievingReleaseInfo(_, _, _, let error) = error { + actualError = error + } else { + actualError = error + } + completion(.failure(RegistryError.failedRetrievingSourceArchiveSignature( + registry: registry, + package: package.underlying, + version: version, + error: actualError + ))) + } } } } diff --git a/Tests/PackageRegistryTests/PackageVersionChecksumTOFUTests.swift b/Tests/PackageRegistryTests/PackageVersionChecksumTOFUTests.swift index 8a70753006b..fdbbe4a69ce 100644 --- a/Tests/PackageRegistryTests/PackageVersionChecksumTOFUTests.swift +++ b/Tests/PackageRegistryTests/PackageVersionChecksumTOFUTests.swift @@ -85,11 +85,7 @@ final class PackageVersionChecksumTOFUTests: XCTestCase { fingerprintCheckingMode: fingerprintCheckingMode ) - let tofu = PackageVersionChecksumTOFU( - fingerprintStorage: fingerprintStorage, - fingerprintCheckingMode: fingerprintCheckingMode, - registryClient: registryClient - ) + let tofu = registryClient.checksumTOFU! // Checksum for package version not found in storage, // so we fetch metadata to get the expected checksum, @@ -180,11 +176,7 @@ final class PackageVersionChecksumTOFUTests: XCTestCase { fingerprintCheckingMode: fingerprintCheckingMode ) - let tofu = PackageVersionChecksumTOFU( - fingerprintStorage: fingerprintStorage, - fingerprintCheckingMode: fingerprintCheckingMode, - registryClient: registryClient - ) + let tofu = registryClient.checksumTOFU! // We get expected checksum from metadata but it's different // from value in storage, and because of .strict mode, @@ -265,12 +257,7 @@ final class PackageVersionChecksumTOFUTests: XCTestCase { fingerprintCheckingMode: fingerprintCheckingMode ) - let tofu = PackageVersionChecksumTOFU( - fingerprintStorage: fingerprintStorage, - fingerprintCheckingMode: fingerprintCheckingMode, - registryClient: registryClient - ) - + let tofu = registryClient.checksumTOFU! let observability = ObservabilitySystem.makeForTesting() // We get expected checksum from metadata and it's different @@ -325,15 +312,8 @@ final class PackageVersionChecksumTOFUTests: XCTestCase { fingerprintCheckingMode: fingerprintCheckingMode ) - let tofu = PackageVersionChecksumTOFU( - fingerprintStorage: fingerprintStorage, - fingerprintCheckingMode: fingerprintCheckingMode, - registryClient: registryClient - ) + let tofu = registryClient.checksumTOFU! - // We get expected checksum from metadata but it's different - // from value in storage, and because of .strict mode, - // an error is thrown. XCTAssertThrowsError( try tofu.validate( registry: registry, @@ -381,15 +361,8 @@ final class PackageVersionChecksumTOFUTests: XCTestCase { fingerprintCheckingMode: fingerprintCheckingMode ) - let tofu = PackageVersionChecksumTOFU( - fingerprintStorage: fingerprintStorage, - fingerprintCheckingMode: fingerprintCheckingMode, - registryClient: registryClient - ) + let tofu = registryClient.checksumTOFU! - // We get expected checksum from metadata but it's different - // from value in storage, and because of .strict mode, - // an error is thrown. XCTAssertThrowsError( try tofu.validate( registry: registry, @@ -431,15 +404,8 @@ final class PackageVersionChecksumTOFUTests: XCTestCase { fingerprintCheckingMode: fingerprintCheckingMode ) - let tofu = PackageVersionChecksumTOFU( - fingerprintStorage: fingerprintStorage, - fingerprintCheckingMode: fingerprintCheckingMode, - registryClient: registryClient - ) + let tofu = registryClient.checksumTOFU! - // We get expected checksum from metadata but it's different - // from value in storage, and because of .strict mode, - // an error is thrown. XCTAssertThrowsError( try tofu.validate( registry: registry, @@ -488,11 +454,7 @@ final class PackageVersionChecksumTOFUTests: XCTestCase { fingerprintCheckingMode: fingerprintCheckingMode ) - let tofu = PackageVersionChecksumTOFU( - fingerprintStorage: fingerprintStorage, - fingerprintCheckingMode: fingerprintCheckingMode, - registryClient: registryClient - ) + let tofu = registryClient.checksumTOFU! // Checksum for package version found in storage, // so we just compare that with the given checksum. @@ -540,11 +502,7 @@ final class PackageVersionChecksumTOFUTests: XCTestCase { fingerprintCheckingMode: fingerprintCheckingMode ) - let tofu = PackageVersionChecksumTOFU( - fingerprintStorage: fingerprintStorage, - fingerprintCheckingMode: fingerprintCheckingMode, - registryClient: registryClient - ) + let tofu = registryClient.checksumTOFU! // Checksum for package version found in storage, // so we just compare that with the given checksum. @@ -598,12 +556,7 @@ final class PackageVersionChecksumTOFUTests: XCTestCase { fingerprintCheckingMode: fingerprintCheckingMode ) - let tofu = PackageVersionChecksumTOFU( - fingerprintStorage: fingerprintStorage, - fingerprintCheckingMode: fingerprintCheckingMode, - registryClient: registryClient - ) - + let tofu = registryClient.checksumTOFU! let observability = ObservabilitySystem.makeForTesting() // Checksum for package version found in storage, diff --git a/Tests/PackageRegistryTests/SignatureValidationTests.swift b/Tests/PackageRegistryTests/SignatureValidationTests.swift new file mode 100644 index 00000000000..746624bc703 --- /dev/null +++ b/Tests/PackageRegistryTests/SignatureValidationTests.swift @@ -0,0 +1,292 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2023 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 the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Basics +import Foundation +import PackageModel +@testable import PackageRegistry +import PackageSigning +import SPMTestSupport +import TSCBasic +import XCTest + +import struct TSCUtility.Version + +final class SignatureValidationTests: XCTestCase { + // TODO: add testUnsignedPackage_shouldPrompt + // TODO: add tests for signed package + + func testUnsignedPackage_shouldError() throws { + let registryURL = URL("https://packages.example.com") + let identity = PackageIdentity.plain("mona.LinkedList") + let package = identity.registry! + let version = Version("1.1.1") + let metadataURL = URL("\(registryURL)/\(package.scope)/\(package.name)/\(version)") + let checksum = "a2ac54cf25fbc1ad0028f03f0aa4b96833b83bb05a14e510892bb27dea4dc812" + + // Get metadata endpoint will be called to see if package version is signed + let handler: LegacyHTTPClient.Handler = { request, _, completion in + switch (request.method, request.url) { + case (.get, metadataURL): + XCTAssertEqual(request.headers.get("Accept").first, "application/vnd.swift.registry.v1+json") + + let data = """ + { + "id": "mona.LinkedList", + "version": "1.1.1", + "resources": [ + { + "name": "source-archive", + "type": "application/zip", + "checksum": "\(checksum)" + } + ], + "metadata": { + "description": "One thing links to another." + } + } + """.data(using: .utf8)! + + completion(.success(.init( + statusCode: 200, + headers: .init([ + .init(name: "Content-Length", value: "\(data.count)"), + .init(name: "Content-Type", value: "application/json"), + .init(name: "Content-Version", value: "1"), + ]), + body: data + ))) + default: + completion(.failure(StringError("method and url should match"))) + } + } + + let httpClient = LegacyHTTPClient(handler: handler) + httpClient.configuration.circuitBreakerStrategy = .none + httpClient.configuration.retryStrategy = .none + + let registry = Registry(url: registryURL, supportsAvailability: false) + var configuration = RegistryConfiguration() + configuration.defaultRegistry = registry + + var signingConfiguration = RegistryConfiguration.Security.Signing() + signingConfiguration.onUnsigned = .error // intended for this test; don't change + configuration.security = RegistryConfiguration.Security( + default: RegistryConfiguration.Security.Global( + signing: signingConfiguration + ) + ) + + let signingEntityStorage = MockPackageSigningEntityStorage() + let signingEntityCheckingMode = SigningEntityCheckingMode.strict + + let registryClient = makeRegistryClient( + configuration: configuration, + httpClient: httpClient, + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode + ) + + let signatureValidation = registryClient.signatureValidation! + + // Package is not signed. With onUnsigned = .error, + // an error gets thrown. + XCTAssertThrowsError( + try signatureValidation.validate( + registry: registry, + package: package, + version: version, + content: Data(emptyZipFile.contents), + configuration: configuration.signing(for: package, registry: registry) + ) + ) { error in + guard case RegistryError.sourceArchiveNotSigned = error else { + return XCTFail("Expected RegistryError.sourceArchiveNotSigned, got '\(error)'") + } + } + } + + func testUnsignedPackage_shouldWarn() throws { + let registryURL = URL("https://packages.example.com") + let identity = PackageIdentity.plain("mona.LinkedList") + let package = identity.registry! + let version = Version("1.1.1") + let metadataURL = URL("\(registryURL)/\(package.scope)/\(package.name)/\(version)") + let checksum = "a2ac54cf25fbc1ad0028f03f0aa4b96833b83bb05a14e510892bb27dea4dc812" + + // Get metadata endpoint will be called to see if package version is signed + let handler: LegacyHTTPClient.Handler = { request, _, completion in + switch (request.method, request.url) { + case (.get, metadataURL): + XCTAssertEqual(request.headers.get("Accept").first, "application/vnd.swift.registry.v1+json") + + let data = """ + { + "id": "mona.LinkedList", + "version": "1.1.1", + "resources": [ + { + "name": "source-archive", + "type": "application/zip", + "checksum": "\(checksum)" + } + ], + "metadata": { + "description": "One thing links to another." + } + } + """.data(using: .utf8)! + + completion(.success(.init( + statusCode: 200, + headers: .init([ + .init(name: "Content-Length", value: "\(data.count)"), + .init(name: "Content-Type", value: "application/json"), + .init(name: "Content-Version", value: "1"), + ]), + body: data + ))) + default: + completion(.failure(StringError("method and url should match"))) + } + } + + let httpClient = LegacyHTTPClient(handler: handler) + httpClient.configuration.circuitBreakerStrategy = .none + httpClient.configuration.retryStrategy = .none + + let registry = Registry(url: registryURL, supportsAvailability: false) + var configuration = RegistryConfiguration() + configuration.defaultRegistry = registry + + var signingConfiguration = RegistryConfiguration.Security.Signing() + signingConfiguration.onUnsigned = .warn // intended for this test; don't change + configuration.security = RegistryConfiguration.Security( + default: RegistryConfiguration.Security.Global( + signing: signingConfiguration + ) + ) + + let signingEntityStorage = MockPackageSigningEntityStorage() + let signingEntityCheckingMode = SigningEntityCheckingMode.strict + + let registryClient = makeRegistryClient( + configuration: configuration, + httpClient: httpClient, + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode + ) + + let signatureValidation = registryClient.signatureValidation! + let observability = ObservabilitySystem.makeForTesting() + + // Package is not signed. With onUnsigned = .warn, + // no error gets thrown but there should be a warning + XCTAssertNoThrow( + try signatureValidation.validate( + registry: registry, + package: package, + version: version, + content: Data(emptyZipFile.contents), + configuration: configuration.signing(for: package, registry: registry), + observabilityScope: observability.topScope + ) + ) + + testDiagnostics(observability.diagnostics) { result in + result.check(diagnostic: .contains("is not signed"), severity: .warning) + } + } + + func testFailedToFetchSignature_shouldError() throws { + let registryURL = URL("https://packages.example.com") + let identity = PackageIdentity.plain("mona.LinkedList") + let package = identity.registry! + let version = Version("1.1.1") + let metadataURL = URL("\(registryURL)/\(package.scope)/\(package.name)/\(version)") + + let serverErrorHandler = ServerErrorHandler( + method: .get, + url: metadataURL, + errorCode: 404, + errorDescription: "not found" + ) + + let httpClient = LegacyHTTPClient(handler: serverErrorHandler.handle) + httpClient.configuration.circuitBreakerStrategy = .none + httpClient.configuration.retryStrategy = .none + + let registry = Registry(url: registryURL, supportsAvailability: false) + var configuration = RegistryConfiguration() + configuration.defaultRegistry = registry + + var signingConfiguration = RegistryConfiguration.Security.Signing() + signingConfiguration.onUnsigned = .error // intended for this test; don't change + configuration.security = RegistryConfiguration.Security( + default: RegistryConfiguration.Security.Global( + signing: signingConfiguration + ) + ) + + let signingEntityStorage = MockPackageSigningEntityStorage() + let signingEntityCheckingMode = SigningEntityCheckingMode.strict + + let registryClient = makeRegistryClient( + configuration: configuration, + httpClient: httpClient, + signingEntityStorage: signingEntityStorage, + signingEntityCheckingMode: signingEntityCheckingMode + ) + + let signatureValidation = registryClient.signatureValidation! + + // Failed to fetch package metadata / signature + XCTAssertThrowsError( + try signatureValidation.validate( + registry: registry, + package: package, + version: version, + content: Data(emptyZipFile.contents), + configuration: configuration.signing(for: package, registry: registry) + ) + ) { error in + guard case RegistryError.failedRetrievingSourceArchiveSignature = error else { + return XCTFail("Expected RegistryError.failedRetrievingSourceArchiveSignature, got '\(error)'") + } + } + } +} + +extension SignatureValidation { + fileprivate func validate( + registry: Registry, + package: PackageIdentity.RegistryIdentity, + version: Version, + content: Data, + configuration: RegistryConfiguration.Security.Signing, + observabilityScope: ObservabilityScope? = nil + ) throws { + try tsc_await { + self.validate( + registry: registry, + package: package, + version: version, + content: content, + configuration: configuration, + timeout: nil, + observabilityScope: observabilityScope ?? ObservabilitySystem.NOOP, + callbackQueue: .sharedConcurrent, + completion: $0 + ) + } + } +} From 593f79b36c46ab61beabffc98246185ce1c48fbf Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Tue, 28 Feb 2023 23:28:47 -0800 Subject: [PATCH 14/20] Fix CI --- Sources/PackageRegistry/SignatureValidation.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/PackageRegistry/SignatureValidation.swift b/Sources/PackageRegistry/SignatureValidation.swift index da8c81af949..f65358c9550 100644 --- a/Sources/PackageRegistry/SignatureValidation.swift +++ b/Sources/PackageRegistry/SignatureValidation.swift @@ -10,6 +10,10 @@ // //===----------------------------------------------------------------------===// +#if swift(>=5.5.2) +import _Concurrency +#endif + import Dispatch import struct Foundation.Data From 8426a0ac5b4f91b22af093c73ca686b5785359af Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Wed, 1 Mar 2023 14:40:01 -0800 Subject: [PATCH 15/20] Add fatalError for TODOs so we don't forget --- Sources/PackageRegistry/SignatureValidation.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/PackageRegistry/SignatureValidation.swift b/Sources/PackageRegistry/SignatureValidation.swift index f65358c9550..cc0ee0233b8 100644 --- a/Sources/PackageRegistry/SignatureValidation.swift +++ b/Sources/PackageRegistry/SignatureValidation.swift @@ -124,9 +124,11 @@ struct SignatureValidation { if case RegistryError.sourceArchiveNotSigned = error { // Source archive is not signed // TODO: Call delegate to prompt user to continue with unsigned package or error. + fatalError("TO BE IMPLEMENTED") } else { // Cannot determine if source archive is signed or not // TODO: Call delegate to prompt user to continue with package as if it were unsigned or error. + fatalError("TO BE IMPLEMENTED") } case .warn: observabilityScope.emit(warning: "\(error)") @@ -184,7 +186,7 @@ struct SignatureValidation { completion(.failure(RegistryError.signerNotTrusted)) case .prompt: // TODO: Call delegate to prompt user to continue with package or error. - () + fatalError("TO BE IMPLEMENTED") case .warn: // TODO: populate error with signer detail observabilityScope.emit(warning: "\(RegistryError.signerNotTrusted)") From d6248cf0cd6e1d65407c8c52e29007c7815fd4b7 Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Wed, 1 Mar 2023 14:58:29 -0800 Subject: [PATCH 16/20] Lowercase diagnostics --- Sources/PackageRegistry/RegistryClient.swift | 90 ++++++++++--------- .../PackageRegistry/SignatureValidation.swift | 2 +- 2 files changed, 48 insertions(+), 44 deletions(-) diff --git a/Sources/PackageRegistry/RegistryClient.swift b/Sources/PackageRegistry/RegistryClient.swift index 4d753b9b346..4f8ce3bc9d9 100644 --- a/Sources/PackageRegistry/RegistryClient.swift +++ b/Sources/PackageRegistry/RegistryClient.swift @@ -34,6 +34,10 @@ public final class RegistryClient: Cancellable { private let authorizationProvider: LegacyHTTPClientConfiguration.AuthorizationProvider? private let jsonDecoder: JSONDecoder + // TODO: Both of these make HTTP call to fetch package version metadata. + // Perhaps one way to optimize is introduce `PackageValidation` that + // wraps around these, and have it fetch the metadata then feed it to each. + // This way we would only fetch metadata once per download archive transaction. private(set) var checksumTOFU: PackageVersionChecksumTOFU! private(set) var signatureValidation: SignatureValidation! @@ -1262,94 +1266,94 @@ public enum RegistryError: Error, CustomStringConvertible { switch self { case .registryNotConfigured(let scope): if let scope = scope { - return "No registry configured for '\(scope)' scope" + return "no registry configured for '\(scope)' scope" } else { - return "No registry configured'" + return "no registry configured'" } case .invalidPackageIdentity(let packageIdentity): - return "Invalid package identifier '\(packageIdentity)'" + return "invalid package identifier '\(packageIdentity)'" case .invalidURL(let url): - return "Invalid URL '\(url)'" + return "invalid URL '\(url)'" case .invalidResponseStatus(let expected, let actual): - return "Invalid registry response status '\(actual)', expected '\(expected)'" + return "invalid registry response status '\(actual)', expected '\(expected)'" case .invalidContentVersion(let expected, let actual): - return "Invalid registry response content version '\(actual ?? "")', expected '\(expected)'" + return "invalid registry response content version '\(actual ?? "")', expected '\(expected)'" case .invalidContentType(let expected, let actual): - return "Invalid registry response content type '\(actual ?? "")', expected '\(expected)'" + return "invalid registry response content type '\(actual ?? "")', expected '\(expected)'" case .invalidResponse: - return "Invalid registry response" + return "invalid registry response" case .missingSourceArchive: - return "Missing registry source archive" + return "missing registry source archive" case .invalidSourceArchive: - return "Invalid registry source archive" + return "invalid registry source archive" case .unsupportedHashAlgorithm(let algorithm): - return "Unsupported hash algorithm '\(algorithm)'" + return "unsupported hash algorithm '\(algorithm)'" case .failedToComputeChecksum(let error): - return "Failed computing registry source archive checksum: \(error)" + return "failed computing registry source archive checksum: \(error)" case .checksumChanged(let latest, let previous): - return "The latest checksum '\(latest)' is different from the previously recorded value '\(previous)'" + return "the latest checksum '\(latest)' is different from the previously recorded value '\(previous)'" case .invalidChecksum(let expected, let actual): - return "Invalid registry source archive checksum '\(actual)', expected '\(expected)'" + return "invalid registry source archive checksum '\(actual)', expected '\(expected)'" case .pathAlreadyExists(let path): - return "Path already exists '\(path)'" + return "path already exists '\(path)'" case .failedRetrievingReleases(let registry, let packageIdentity, let error): - return "Failed fetching '\(packageIdentity)' releases list from '\(registry)': \(error)" + return "failed fetching '\(packageIdentity)' releases list from '\(registry)': \(error)" case .failedRetrievingReleaseInfo(let registry, let packageIdentity, let version, let error): - return "Failed fetching '\(packageIdentity)@\(version)' release information from '\(registry)': \(error)" + return "failed fetching '\(packageIdentity)@\(version)' release information from '\(registry)': \(error)" case .failedRetrievingReleaseChecksum(let registry, let packageIdentity, let version, let error): - return "Failed fetching '\(packageIdentity)@\(version)' release checksum from '\(registry)': \(error)" + return "failed fetching '\(packageIdentity)@\(version)' release checksum from '\(registry)': \(error)" case .failedRetrievingManifest(let registry, let packageIdentity, let version, let error): - return "Failed retrieving '\(packageIdentity)@\(version)' manifest from '\(registry)': \(error)" + return "failed retrieving '\(packageIdentity)@\(version)' manifest from '\(registry)': \(error)" case .failedDownloadingSourceArchive(let registry, let packageIdentity, let version, let error): - return "Failed downloading '\(packageIdentity)@\(version)' source archive from '\(registry)': \(error)" + return "failed downloading '\(packageIdentity)@\(version)' source archive from '\(registry)': \(error)" case .failedIdentityLookup(let registry, let scmURL, let error): - return "Failed looking up identity for '\(scmURL)' on '\(registry)': \(error)" + return "failed looking up identity for '\(scmURL)' on '\(registry)': \(error)" case .failedLoadingPackageArchive(let path): - return "Failed loading package archive at '\(path)' for publishing" + return "failed loading package archive at '\(path)' for publishing" case .failedLoadingPackageMetadata(let path): - return "Failed loading package metadata at '\(path)' for publishing" + return "failed loading package metadata at '\(path)' for publishing" case .failedPublishing(let error): - return "Failed publishing: \(error)" + return "failed publishing: \(error)" case .missingPublishingLocation: - return "Response missing registry source archive" + return "response missing registry source archive" case .serverError(let code, let details): - return "Server error \(code): \(details)" + return "server error \(code): \(details)" case .unauthorized: - return "Missing or invalid authentication credentials" + return "missing or invalid authentication credentials" case .authenticationMethodNotSupported: - return "Authentication method not supported" + return "authentication method not supported" case .forbidden: - return "Forbidden" + return "forbidden" case .registryNotAvailable(let registry): - return "Registry at '\(registry.url)' is not available at this time, please try again later" + return "registry at '\(registry.url)' is not available at this time, please try again later" case .packageNotFound: - return "Package not found on registry" + return "package not found on registry" case .packageVersionNotFound: - return "Package version not found on registry" + return "package version not found on registry" case .sourceArchiveNotSigned(let registry, let packageIdentity, let version): return "'\(packageIdentity)@\(version)' source archive from '\(registry)' is not signed" case .failedLoadingSignature: - return "Failed loading signature for validation" + return "failed loading signature for validation" case .failedRetrievingSourceArchiveSignature(let registry, let packageIdentity, let version, let error): - return "Failed retrieving '\(packageIdentity)@\(version)' source archive signature from '\(registry)': \(error)" + return "failed retrieving '\(packageIdentity)@\(version)' source archive signature from '\(registry)': \(error)" case .missingConfiguration(let details): - return "Unable to proceed because of missing configuration: \(details)" + return "unable to proceed because of missing configuration: \(details)" case .missingSignatureFormat: - return "Missing signature format" + return "missing signature format" case .unknownSignatureFormat(let format): - return "Unknown signature format: \(format)" + return "unknown signature format: \(format)" case .invalidSignature(let reason): - return "Signature is invalid: \(reason)" + return "signature is invalid: \(reason)" case .invalidSigningCertificate(let reason): - return "The signing certificate is invalid: \(reason)" + return "the signing certificate is invalid: \(reason)" case .signerNotTrusted: - return "The signer is not trusted" + return "the signer is not trusted" case .failedToValidateSignature(let error): - return "Failed to validate signature: \(error)" + return "failed to validate signature: \(error)" case .signingEntityForReleaseChanged(let package, let version, let latest, let previous): - return "The signing entity '\(String(describing: latest))' for '\(package)@\(version)' is different from the previously recorded value '\(previous)'" + return "the signing entity '\(String(describing: latest))' for '\(package)@\(version)' is different from the previously recorded value '\(previous)'" case .signingEntityForPackageChanged(let package, let latest, let previous): - return "The signing entity '\(String(describing: latest))' for '\(package)' is different from the previously recorded value '\(previous)'" + return "the signing entity '\(String(describing: latest))' for '\(package)' is different from the previously recorded value '\(previous)'" } } } diff --git a/Sources/PackageRegistry/SignatureValidation.swift b/Sources/PackageRegistry/SignatureValidation.swift index cc0ee0233b8..9926f5d102f 100644 --- a/Sources/PackageRegistry/SignatureValidation.swift +++ b/Sources/PackageRegistry/SignatureValidation.swift @@ -201,7 +201,7 @@ struct SignatureValidation { } } #else - completion(.success(.none)) + completion(.failure(InternalError("package signature validation not supported"))) #endif } From 6eca9dbdfee13f6085d598d8a3338ff2cd1d1fe0 Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Wed, 1 Mar 2023 17:51:04 -0800 Subject: [PATCH 17/20] Fix tests --- Tests/WorkspaceTests/WorkspaceTests.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Tests/WorkspaceTests/WorkspaceTests.swift b/Tests/WorkspaceTests/WorkspaceTests.swift index 5e6c5b555ae..22720132b99 100644 --- a/Tests/WorkspaceTests/WorkspaceTests.swift +++ b/Tests/WorkspaceTests/WorkspaceTests.swift @@ -12387,7 +12387,7 @@ final class WorkspaceTests: XCTestCase { workspace.checkPackageGraphFailure(roots: ["MyPackage"]) { diagnostics in testDiagnostics(diagnostics) { result in - result.check(diagnostic: .equal("No registry configured for 'org' scope"), severity: .error) + result.check(diagnostic: .equal("no registry configured for 'org' scope"), severity: .error) } } } @@ -12443,7 +12443,7 @@ final class WorkspaceTests: XCTestCase { workspace.registryClient = registryClient workspace.checkPackageGraphFailure(roots: ["MyPackage"]) { diagnostics in testDiagnostics(diagnostics) { result in - result.check(diagnostic: .equal("Failed fetching 'org.foo' releases list from 'http://localhost': boom"), severity: .error) + result.check(diagnostic: .equal("failed fetching 'org.foo' releases list from 'http://localhost': boom"), severity: .error) } } } @@ -12462,7 +12462,7 @@ final class WorkspaceTests: XCTestCase { workspace.registryClient = registryClient workspace.checkPackageGraphFailure(roots: ["MyPackage"]) { diagnostics in testDiagnostics(diagnostics) { result in - result.check(diagnostic: .equal("Failed fetching 'org.foo' releases list from 'http://localhost': Server error 500: Internal Server Error"), severity: .error) + result.check(diagnostic: .equal("failed fetching 'org.foo' releases list from 'http://localhost': server error 500: Internal Server Error"), severity: .error) } } } @@ -12519,7 +12519,7 @@ final class WorkspaceTests: XCTestCase { workspace.registryClient = registryClient workspace.checkPackageGraphFailure(roots: ["MyPackage"]) { diagnostics in testDiagnostics(diagnostics) { result in - result.check(diagnostic: .equal("Failed fetching 'org.foo@1.0.0' release checksum from 'http://localhost': boom"), severity: .error) + result.check(diagnostic: .equal("failed fetching 'org.foo@1.0.0' release checksum from 'http://localhost': boom"), severity: .error) } } } @@ -12538,7 +12538,7 @@ final class WorkspaceTests: XCTestCase { workspace.registryClient = registryClient workspace.checkPackageGraphFailure(roots: ["MyPackage"]) { diagnostics in testDiagnostics(diagnostics) { result in - result.check(diagnostic: .equal("Failed fetching 'org.foo@1.0.0' release checksum from 'http://localhost': Server error 500: Internal Server Error"), severity: .error) + result.check(diagnostic: .equal("failed fetching 'org.foo@1.0.0' release checksum from 'http://localhost': server error 500: Internal Server Error"), severity: .error) } } } @@ -12595,7 +12595,7 @@ final class WorkspaceTests: XCTestCase { workspace.registryClient = registryClient workspace.checkPackageGraphFailure(roots: ["MyPackage"]) { diagnostics in testDiagnostics(diagnostics) { result in - result.check(diagnostic: .equal("Failed retrieving 'org.foo@1.0.0' manifest from 'http://localhost': boom"), severity: .error) + result.check(diagnostic: .equal("failed retrieving 'org.foo@1.0.0' manifest from 'http://localhost': boom"), severity: .error) } } } @@ -12614,7 +12614,7 @@ final class WorkspaceTests: XCTestCase { workspace.registryClient = registryClient workspace.checkPackageGraphFailure(roots: ["MyPackage"]) { diagnostics in testDiagnostics(diagnostics) { result in - result.check(diagnostic: .equal("Failed retrieving 'org.foo@1.0.0' manifest from 'http://localhost': Server error 500: Internal Server Error"), severity: .error) + result.check(diagnostic: .equal("failed retrieving 'org.foo@1.0.0' manifest from 'http://localhost': server error 500: Internal Server Error"), severity: .error) } } } @@ -12671,7 +12671,7 @@ final class WorkspaceTests: XCTestCase { workspace.registryClient = registryClient workspace.checkPackageGraphFailure(roots: ["MyPackage"]) { diagnostics in testDiagnostics(diagnostics) { result in - result.check(diagnostic: .equal("Failed downloading 'org.foo@1.0.0' source archive from 'http://localhost': boom"), severity: .error) + result.check(diagnostic: .equal("failed downloading 'org.foo@1.0.0' source archive from 'http://localhost': boom"), severity: .error) } } } @@ -12690,7 +12690,7 @@ final class WorkspaceTests: XCTestCase { workspace.registryClient = registryClient workspace.checkPackageGraphFailure(roots: ["MyPackage"]) { diagnostics in testDiagnostics(diagnostics) { result in - result.check(diagnostic: .equal("Failed downloading 'org.foo@1.0.0' source archive from 'http://localhost': Server error 500: Internal Server Error"), severity: .error) + result.check(diagnostic: .equal("failed downloading 'org.foo@1.0.0' source archive from 'http://localhost': server error 500: Internal Server Error"), severity: .error) } } } From d67f64ef0a4e9fcbaaf2c39589598995476e3ad3 Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Wed, 1 Mar 2023 21:12:06 -0800 Subject: [PATCH 18/20] Add swift-crypto back to bootstrap --- Utilities/Docker/docker-compose.yaml | 3 ++- Utilities/bootstrap | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Utilities/Docker/docker-compose.yaml b/Utilities/Docker/docker-compose.yaml index c2ef5f721a3..6d1d0de820d 100644 --- a/Utilities/Docker/docker-compose.yaml +++ b/Utilities/Docker/docker-compose.yaml @@ -1,6 +1,6 @@ # This source file is part of the Swift open source project # -# Copyright (c) 2021 Apple Inc. and the Swift project authors +# Copyright (c) 2021-2023 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 @@ -36,6 +36,7 @@ services: - ../../../swift-tools-support-core:/code/swift-tools-support-core:z - ../../../yams:/code/yams:z - ../../../swift-argument-parser:/code/swift-argument-parser:z + - ../../../swift-crypto:/code/swift-crypto:z - ../../../swift-driver:/code/swift-driver:z - ../../../swift-llbuild:/code/llbuild:z - ../../../swift-system:/code/swift-system:z diff --git a/Utilities/bootstrap b/Utilities/bootstrap index 746c05ab1a0..e2ddfab1bae 100755 --- a/Utilities/bootstrap +++ b/Utilities/bootstrap @@ -4,7 +4,7 @@ """ This source file is part of the Swift open source project // -// Copyright (c) 2014-2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014-2023 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 @@ -184,6 +184,7 @@ def parse_global_args(args): args.source_dirs["tsc"] = os.path.join(args.project_root, "..", "swift-tools-support-core") args.source_dirs["yams"] = os.path.join(args.project_root, "..", "yams") args.source_dirs["swift-argument-parser"] = os.path.join(args.project_root, "..", "swift-argument-parser") + args.source_dirs["swift-crypto"] = os.path.join(args.project_root, "..", "swift-crypto") args.source_dirs["swift-driver"] = os.path.join(args.project_root, "..", "swift-driver") args.source_dirs["swift-system"] = os.path.join(args.project_root, "..", "swift-system") args.source_dirs["swift-collections"] = os.path.join(args.project_root, "..", "swift-collections") @@ -351,6 +352,7 @@ def build(args): ] build_dependency(args, "swift-driver", swift_driver_cmake_flags) build_dependency(args, "swift-collections") + build_dependency(args, "swift-crypto") build_swiftpm_with_cmake(args) build_swiftpm_with_swiftpm(args,integrated_swift_driver=False) @@ -594,6 +596,7 @@ def build_swiftpm_with_cmake(args): get_llbuild_cmake_arg(args), "-DTSC_DIR=" + os.path.join(args.build_dirs["tsc"], "cmake/modules"), "-DArgumentParser_DIR=" + os.path.join(args.build_dirs["swift-argument-parser"], "cmake/modules"), + "-DSwiftCrypto_DIR=" + os.path.join(args.build_dirs["swift-crypto"], "cmake/modules"), "-DSwiftDriver_DIR=" + os.path.join(args.build_dirs["swift-driver"], "cmake/modules"), "-DSwiftSystem_DIR=" + os.path.join(args.build_dirs["swift-system"], "cmake/modules"), "-DSwiftCollections_DIR=" + os.path.join(args.build_dirs["swift-collections"], "cmake/modules"), @@ -611,6 +614,7 @@ def build_swiftpm_with_cmake(args): if platform.system() == "Darwin": add_rpath_for_cmake_build(args, os.path.join(args.build_dirs["yams"], "lib")) add_rpath_for_cmake_build(args, os.path.join(args.build_dirs["swift-argument-parser"], "lib")) + add_rpath_for_cmake_build(args, os.path.join(args.build_dirs["swift-crypto"], "lib")) add_rpath_for_cmake_build(args, os.path.join(args.build_dirs["swift-driver"], "lib")) add_rpath_for_cmake_build(args, os.path.join(args.build_dirs["swift-system"], "lib")) add_rpath_for_cmake_build(args, os.path.join(args.build_dirs["swift-collections"], "lib")) @@ -724,6 +728,7 @@ def get_swiftpm_env_cmd(args): os.path.join(args.build_dirs["llbuild"], "lib"), os.path.join(args.build_dirs["yams"], "lib"), os.path.join(args.build_dirs["swift-argument-parser"], "lib"), + os.path.join(args.build_dirs["swift-crypto"], "lib"), os.path.join(args.build_dirs["swift-driver"], "lib"), os.path.join(args.build_dirs["swift-system"], "lib"), os.path.join(args.build_dirs["swift-collections"], "lib"), From 3e76c405e82bcf30fc7ef6f1fd67939d1f0d4bf4 Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Thu, 2 Mar 2023 09:55:08 -0800 Subject: [PATCH 19/20] Fix Windows build --- CMakeLists.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 28f9ee7378a..17580f21090 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,7 +54,12 @@ if(FIND_PM_DEPS) find_package(ArgumentParser CONFIG REQUIRED) find_package(SwiftDriver CONFIG REQUIRED) find_package(SwiftCollections CONFIG REQUIRED) - find_package(SwiftCrypto CONFIG REQUIRED) + find_package(SwiftCrypto CONFIG) + if (SwiftCrypto_FOUND) + add_compile_options($<$:-DCRYPTO_v2>) + else() + message(WARNING "SwiftCrypto not found, package collections and package signing will be unavailable") + endif() endif() find_package(dispatch QUIET) From 8009de9decad644d36f4696143dd96195fd6d6d0 Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Thu, 2 Mar 2023 14:30:14 -0800 Subject: [PATCH 20/20] Make 'import Crypto' implementation only --- Sources/PackageCollectionsSigning/Key/Key+EC.swift | 4 ++-- Sources/PackageCollectionsSigning/Signing/Signing+ECKey.swift | 4 ++-- Sources/PackageSigning/SignatureProvider.swift | 2 +- Sources/PackageSigning/SigningIdentity.swift | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/PackageCollectionsSigning/Key/Key+EC.swift b/Sources/PackageCollectionsSigning/Key/Key+EC.swift index 1f031799d21..d6f586e825b 100644 --- a/Sources/PackageCollectionsSigning/Key/Key+EC.swift +++ b/Sources/PackageCollectionsSigning/Key/Key+EC.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021-2023 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 @@ -12,7 +12,7 @@ import Foundation -import Crypto +@_implementationOnly import Crypto typealias CryptoECPrivateKey = P256.Signing.PrivateKey typealias CryptoECPublicKey = P256.Signing.PublicKey diff --git a/Sources/PackageCollectionsSigning/Signing/Signing+ECKey.swift b/Sources/PackageCollectionsSigning/Signing/Signing+ECKey.swift index dbca56000db..2da5a6fd468 100644 --- a/Sources/PackageCollectionsSigning/Signing/Signing+ECKey.swift +++ b/Sources/PackageCollectionsSigning/Signing/Signing+ECKey.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021-2023 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 @@ -12,7 +12,7 @@ import struct Foundation.Data -import Crypto +@_implementationOnly import Crypto // MARK: - MessageSigner and MessageValidator conformance diff --git a/Sources/PackageSigning/SignatureProvider.swift b/Sources/PackageSigning/SignatureProvider.swift index 2a04771e560..78df0bf4cea 100644 --- a/Sources/PackageSigning/SignatureProvider.swift +++ b/Sources/PackageSigning/SignatureProvider.swift @@ -17,7 +17,7 @@ import Security #endif import Basics -import Crypto +@_implementationOnly import Crypto public enum SignatureProvider { public static func sign( diff --git a/Sources/PackageSigning/SigningIdentity.swift b/Sources/PackageSigning/SigningIdentity.swift index d9e3666cc3e..458784aa370 100644 --- a/Sources/PackageSigning/SigningIdentity.swift +++ b/Sources/PackageSigning/SigningIdentity.swift @@ -17,7 +17,7 @@ import Security #endif import Basics -import Crypto +@_implementationOnly import Crypto public protocol SigningIdentity { // TODO: change type to Certificate