Skip to content

Commit a6fde25

Browse files
authored
Expected signing entity verification (#6359)
This allows clients to pass in a dictionary with expected signing entities that SwiftPM will check after loading the package graph. This can be used by clients to provide a priori configuration of expected signing by the user or provide a way to verify that information that was previously shown to users matches what was verified during signature verification. Note that since this operates at the workspace level, we're verifying against the data cached during signature verification, not against the actual data. rdar://107162424
1 parent 45bf4e8 commit a6fde25

File tree

5 files changed

+366
-6
lines changed

5 files changed

+366
-6
lines changed

Sources/PackageModel/RegistryReleaseMetadata.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public struct RegistryReleaseMetadata {
105105
}
106106
}
107107

108-
public enum SigningEntity: Codable {
108+
public enum SigningEntity: Codable, Equatable {
109109
case recognized(type: String, commonName: String?, organization: String?, identity: String?)
110110
case unrecognized(commonName: String?, organization: String?)
111111
}

Sources/SPMTestSupport/MockPackage.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public struct MockPackage {
7676
platforms: [PlatformDescription] = [],
7777
identity: String,
7878
alternativeURLs: [String]? = .none,
79+
metadata: RegistryReleaseMetadata? = .none,
7980
targets: [MockTarget],
8081
products: [MockProduct],
8182
dependencies: [MockDependency] = [],
@@ -85,7 +86,11 @@ public struct MockPackage {
8586
) {
8687
self.name = name
8788
self.platforms = platforms
88-
self.location = .registry(identity: .plain(identity), alternativeURLs: alternativeURLs?.compactMap{ URL(string: $0) })
89+
self.location = .registry(
90+
identity: .plain(identity),
91+
alternativeURLs: alternativeURLs?.compactMap{ URL(string: $0) },
92+
metadata: metadata
93+
)
8994
self.targets = targets
9095
self.products = products
9196
self.dependencies = dependencies
@@ -110,6 +115,6 @@ public struct MockPackage {
110115
public enum Location {
111116
case fileSystem(path: RelativePath)
112117
case sourceControl(url: URL)
113-
case registry(identity: PackageIdentity, alternativeURLs: [URL]?)
118+
case registry(identity: PackageIdentity, alternativeURLs: [URL]?, metadata: RegistryReleaseMetadata?)
114119
}
115120
}

Sources/SPMTestSupport/MockWorkspace.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,15 @@ public final class MockWorkspace {
152152
} else {
153153
packagePath = basePath.appending(components: "sourceControl", url.absoluteString.spm_mangledToC99ExtendedIdentifier())
154154
}
155-
case .registry(let identity, _):
155+
case .registry(let identity, _, let metadata):
156156
packagePath = basePath.appending(components: "registry", identity.description.spm_mangledToC99ExtendedIdentifier())
157+
158+
// Write registry release metadata if the mock package provided it.
159+
if let metadata = metadata {
160+
try self.fileSystem.createDirectory(packagePath, recursive: true)
161+
let path = packagePath.appending(component: RegistryReleaseMetadataStorage.fileName)
162+
try RegistryReleaseMetadataStorage.save(metadata, to: path, fileSystem: self.fileSystem)
163+
}
157164
}
158165

159166
let packageLocation: String
@@ -177,7 +184,7 @@ public final class MockWorkspace {
177184
packageLocation = url.absoluteString
178185
packageKind = .remoteSourceControl(url)
179186
sourceControlSpecifier = RepositorySpecifier(url: url)
180-
case (_, .registry(let identity, let alternativeURLs)):
187+
case (_, .registry(let identity, let alternativeURLs, _)):
181188
packageLocation = identity.description
182189
packageKind = .registry(identity)
183190
registryIdentity = identity
@@ -437,6 +444,7 @@ public final class MockWorkspace {
437444
roots: [String] = [],
438445
dependencies: [PackageDependency] = [],
439446
forceResolvedVersions: Bool = false,
447+
expectedSigningEntities: [PackageIdentity: RegistryReleaseMetadata.SigningEntity] = [:],
440448
_ result: (PackageGraph, [Basics.Diagnostic]) throws -> Void
441449
) throws {
442450
let observability = ObservabilitySystem.makeForTesting()
@@ -448,6 +456,7 @@ public final class MockWorkspace {
448456
let graph = try workspace.loadPackageGraph(
449457
rootInput: rootInput,
450458
forceResolvedVersions: forceResolvedVersions,
459+
expectedSigningEntities: expectedSigningEntities,
451460
observabilityScope: observability.topScope
452461
)
453462
try result(graph, observability.diagnostics)

Sources/Workspace/Workspace.swift

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,7 @@ extension Workspace {
10741074
forceResolvedVersions: Bool = false,
10751075
customXCTestMinimumDeploymentTargets: [PackageModel.Platform: PlatformVersion]? = .none,
10761076
testEntryPointPath: AbsolutePath? = nil,
1077+
expectedSigningEntities: [PackageIdentity: RegistryReleaseMetadata.SigningEntity] = [:],
10771078
observabilityScope: ObservabilityScope
10781079
) throws -> PackageGraph {
10791080
// reload state in case it was modified externally (eg by another process) before reloading the graph
@@ -1104,7 +1105,7 @@ extension Workspace {
11041105
}
11051106

11061107
// Load the graph.
1107-
return try PackageGraph.load(
1108+
let packageGraph = try PackageGraph.load(
11081109
root: manifests.root,
11091110
identityResolver: self.identityResolver,
11101111
additionalFileRules: self.configuration.additionalFileRules,
@@ -1119,6 +1120,57 @@ extension Workspace {
11191120
fileSystem: fileSystem,
11201121
observabilityScope: observabilityScope
11211122
)
1123+
1124+
try expectedSigningEntities.forEach { identity, expectedSigningEntity in
1125+
if let package = packageGraph.packages.first(where: { $0.identity == identity }) {
1126+
if let actualSigningEntity = package.registryMetadata?.signature?.signedBy {
1127+
if actualSigningEntity != expectedSigningEntity {
1128+
throw SigningError.mismatchedSigningEntity(
1129+
package: identity,
1130+
expected: expectedSigningEntity,
1131+
actual: actualSigningEntity
1132+
)
1133+
}
1134+
} else {
1135+
throw SigningError.unsigned(package: identity, expected: expectedSigningEntity)
1136+
}
1137+
} else {
1138+
if let mirror = self.mirrors.mirror(for: identity.description) {
1139+
let mirroredIdentity = PackageIdentity.plain(mirror)
1140+
if mirroredIdentity.isRegistry {
1141+
if let package = packageGraph.packages.first(where: { $0.identity == mirroredIdentity }) {
1142+
if let actualSigningEntity = package.registryMetadata?.signature?.signedBy {
1143+
if actualSigningEntity != expectedSigningEntity {
1144+
throw SigningError.mismatchedSigningEntity(
1145+
package: identity,
1146+
expected: expectedSigningEntity,
1147+
actual: actualSigningEntity
1148+
)
1149+
}
1150+
} else {
1151+
throw SigningError.unsigned(package: identity, expected: expectedSigningEntity)
1152+
}
1153+
} else {
1154+
// Unsure if this case is reachable in practice.
1155+
throw SigningError.expectedIdentityNotFound(package: identity)
1156+
}
1157+
} else {
1158+
throw SigningError.expectedSignedMirroredToSourceControl(package: identity, expected: expectedSigningEntity)
1159+
}
1160+
} else {
1161+
throw SigningError.expectedIdentityNotFound(package: identity)
1162+
}
1163+
}
1164+
}
1165+
1166+
return packageGraph
1167+
}
1168+
1169+
public enum SigningError: Swift.Error {
1170+
case expectedIdentityNotFound(package: PackageIdentity)
1171+
case expectedSignedMirroredToSourceControl(package: PackageIdentity, expected: RegistryReleaseMetadata.SigningEntity)
1172+
case mismatchedSigningEntity(package: PackageIdentity, expected: RegistryReleaseMetadata.SigningEntity, actual: RegistryReleaseMetadata.SigningEntity)
1173+
case unsigned(package: PackageIdentity, expected: RegistryReleaseMetadata.SigningEntity)
11221174
}
11231175

11241176
@discardableResult

0 commit comments

Comments
 (0)