Skip to content

Commit 0837576

Browse files
authored
Updates to PackageSigningEntityStorage and signing entity TOFU errors (#6268)
- Storage now includes source entity origin (i.e., registry) - Enhance storage data model to support more sophisticated use cases - Add more details to `RegistryError.signingEntityForReleaseChanged` and `.signingEntityForPackageChanged`
1 parent aa6e0e6 commit 0837576

File tree

8 files changed

+1289
-203
lines changed

8 files changed

+1289
-203
lines changed

Sources/PackageRegistry/RegistryClient.swift

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -809,7 +809,8 @@ public final class RegistryClient: Cancellable {
809809
switch checksumResult {
810810
case .success:
811811
do {
812-
// validate that the destination does not already exist (again, as this
812+
// validate that the destination does not already exist
813+
// (again, as this
813814
// is
814815
// async)
815816
guard !fileSystem.exists(destinationPath) else {
@@ -1344,12 +1345,20 @@ public enum RegistryError: Error, CustomStringConvertible {
13441345
case signerNotTrusted(SigningEntity)
13451346
case failedToValidateSignature(Error)
13461347
case signingEntityForReleaseChanged(
1348+
registry: Registry,
13471349
package: PackageIdentity,
13481350
version: Version,
13491351
latest: SigningEntity?,
13501352
previous: SigningEntity
13511353
)
1352-
case signingEntityForPackageChanged(package: PackageIdentity, latest: SigningEntity?, previous: SigningEntity)
1354+
case signingEntityForPackageChanged(
1355+
registry: Registry,
1356+
package: PackageIdentity,
1357+
version: Version,
1358+
latest: SigningEntity?,
1359+
previous: SigningEntity,
1360+
previousVersion: Version
1361+
)
13531362

13541363
public var description: String {
13551364
switch self {
@@ -1443,10 +1452,17 @@ public enum RegistryError: Error, CustomStringConvertible {
14431452
return "the signer \(signingEntity) is not trusted"
14441453
case .failedToValidateSignature(let error):
14451454
return "failed to validate signature: \(error)"
1446-
case .signingEntityForReleaseChanged(let package, let version, let latest, let previous):
1447-
return "the signing entity '\(String(describing: latest))' for \(package) version \(version) is different from the previously recorded value '\(previous)'"
1448-
case .signingEntityForPackageChanged(let package, let latest, let previous):
1449-
return "the signing entity '\(String(describing: latest))' for \(package) is different from the previously recorded value '\(previous)'"
1455+
case .signingEntityForReleaseChanged(let registry, let package, let version, let latest, let previous):
1456+
return "the signing entity '\(String(describing: latest))' from \(registry) for \(package) version \(version) is different from the previously recorded value '\(previous)'"
1457+
case .signingEntityForPackageChanged(
1458+
let registry,
1459+
let package,
1460+
let version,
1461+
let latest,
1462+
let previous,
1463+
let previousVersion
1464+
):
1465+
return "the signing entity '\(String(describing: latest))' from \(registry) for \(package) version \(version) is different from the previously recorded value '\(previous)' for version \(previousVersion)"
14501466
}
14511467
}
14521468
}

Sources/PackageRegistry/SignatureValidation.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ struct SignatureValidation {
7777
// Always do signing entity TOFU check at the end,
7878
// whether the package is signed or not.
7979
self.signingEntityTOFU.validate(
80+
registry: registry,
8081
package: package,
8182
version: version,
8283
signingEntity: signingEntity,

Sources/PackageRegistry/SigningEntityTOFU.swift

Lines changed: 105 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ struct PackageSigningEntityTOFU {
3131
}
3232

3333
func validate(
34+
registry: Registry,
3435
package: PackageIdentity.RegistryIdentity,
3536
version: Version,
3637
signingEntity: SigningEntity?,
@@ -48,12 +49,13 @@ struct PackageSigningEntityTOFU {
4849
callbackQueue: callbackQueue
4950
) { result in
5051
switch result {
51-
case .success(let signerVersions):
52+
case .success(let packageSigners):
5253
self.validateSigningEntity(
54+
registry: registry,
5355
package: package,
5456
version: version,
5557
signingEntity: signingEntity,
56-
signerVersions: signerVersions,
58+
packageSigners: packageSigners,
5759
observabilityScope: observabilityScope
5860
) { validateResult in
5961
switch validateResult {
@@ -63,6 +65,7 @@ struct PackageSigningEntityTOFU {
6365
return completion(.success(()))
6466
}
6567
self.writeToStorage(
68+
registry: registry,
6669
package: package,
6770
version: version,
6871
signingEntity: signingEntity,
@@ -82,36 +85,40 @@ struct PackageSigningEntityTOFU {
8285
}
8386

8487
private func validateSigningEntity(
88+
registry: Registry,
8589
package: PackageIdentity.RegistryIdentity,
8690
version: Version,
8791
signingEntity: SigningEntity?,
88-
signerVersions: [SigningEntity: Set<Version>],
92+
packageSigners: PackageSigners,
8993
observabilityScope: ObservabilityScope,
9094
completion: @escaping (Result<Bool, Error>) -> Void
9195
) {
9296
// Package is never signed.
9397
// If signingEntity is nil, it means package remains unsigned, which is OK. (none -> none)
94-
// Otherwise, package has gained a signing entity, which is also OK. (none -> some)
95-
if signerVersions.isEmpty {
98+
// Otherwise, package has gained a signer, which is also OK. (none -> some)
99+
if packageSigners.isEmpty {
96100
return completion(.success(true))
97101
}
98102

99103
// If we get to this point, it means we have seen a signed version of the package.
100104

101-
// TODO: It's possible that some of the signing entity changes are legitimate:
102-
// e.g., change of package ownership, package author decides to stop signing releases, etc.
103-
// Instead of failing, we should allow and prompt user to add/replace/remove signing entity.
104-
105-
// We recorded the version's signer previously
106-
if let signerForVersion = signerVersions.signingEntity(of: version) {
107-
// The given signer is different
108-
// TODO: This could indicate a legitimate change in package ownership
109-
guard signerForVersion == signingEntity else {
110-
return self.handleSigningEntityChanged(
105+
let signingEntitiesForVersion = packageSigners.signingEntities(of: version)
106+
// We recorded the version's signer(s) previously
107+
if !signingEntitiesForVersion.isEmpty {
108+
guard let signingEntityToCheck = signingEntity,
109+
signingEntitiesForVersion.contains(signingEntityToCheck)
110+
else {
111+
// The given signer is nil or different
112+
// TODO: This could indicate a legitimate change
113+
// - If signingEntity is nil, it could mean the package author has stopped signing the package.
114+
// - If signingEntity is non-nil, it could mean the package has changed ownership and the new owner
115+
// is re-signing all of the package versions.
116+
return self.handleSigningEntityForPackageVersionChanged(
117+
registry: registry,
111118
package: package,
112119
version: version,
113120
latest: signingEntity,
114-
existing: signerForVersion,
121+
existing: signingEntitiesForVersion.first!, // !-safe since signingEntitiesForVersion is non-empty
115122
observabilityScope: observabilityScope
116123
) { result in
117124
completion(result.tryMap { false })
@@ -121,28 +128,75 @@ struct PackageSigningEntityTOFU {
121128
return completion(.success(false))
122129
}
123130

131+
// Check signer(s) of other version(s)
124132
switch signingEntity {
125133
// Is the package changing from one signer to another?
126134
case .some(let signingEntity):
127-
// Check signer(s) of other version(s)
128-
if let otherSigner = signerVersions.keys.filter({ $0 != signingEntity }).first {
129-
// There is a different signer
130-
// TODO: This could indicate a legitimate change in package ownership
131-
self.handleSigningEntityChanged(
135+
// Does the package have an expected signer?
136+
if let expectedSigner = packageSigners.expectedSigner,
137+
version >= expectedSigner.fromVersion
138+
{
139+
// Signer is as expected
140+
if signingEntity == expectedSigner.signingEntity {
141+
return completion(.success(true))
142+
}
143+
// If the signer is different from expected but has been seen before,
144+
// we allow versions before its highest known version to be signed
145+
// by this signer. This is to handle the case where a signer was recorded
146+
// before expectedSigner is set, and it had signed a version newer than
147+
// expectedSigner.fromVersion. For example, if signer A is recorded to have
148+
// signed v2.0 and later expectedSigner is set to signer B with fromVersion
149+
// set to v1.5, then it should not be a TOFU failure if we see signer A
150+
// for v1.9.
151+
if let knownSigner = packageSigners.signers[signingEntity],
152+
let highestKnownVersion = knownSigner.versions.sorted(by: >).first,
153+
version < highestKnownVersion
154+
{
155+
return completion(.success(true))
156+
}
157+
// Different signer than expected
158+
self.handleSigningEntityForPackageChanged(
159+
registry: registry,
132160
package: package,
161+
version: version,
133162
latest: signingEntity,
134-
existing: otherSigner,
163+
existing: expectedSigner.signingEntity,
164+
existingVersion: expectedSigner.fromVersion,
135165
observabilityScope: observabilityScope
136166
) { result in
137167
completion(result.tryMap { false })
138168
}
139169
} else {
170+
// There might be other signers, but if we have seen this signer before, allow it.
171+
if packageSigners.signers[signingEntity] != nil {
172+
return completion(.success(true))
173+
}
174+
175+
let otherSigningEntities = packageSigners.signers.keys.filter { $0 != signingEntity }
176+
for otherSigningEntity in otherSigningEntities {
177+
// We have not seen this signer before, and there is at least one other signer already.
178+
// TODO: This could indicate a legitimate change in package ownership
179+
if let existingVersion = packageSigners.signers[otherSigningEntity]?.versions.sorted(by: >).first {
180+
return self.handleSigningEntityForPackageChanged(
181+
registry: registry,
182+
package: package,
183+
version: version,
184+
latest: signingEntity,
185+
existing: otherSigningEntity,
186+
existingVersion: existingVersion,
187+
observabilityScope: observabilityScope
188+
) { result in
189+
completion(result.tryMap { false })
190+
}
191+
}
192+
}
193+
140194
// Package doesn't have any other signer besides the given one, which is good.
141195
completion(.success(true))
142196
}
143197
// Or is the package going from having a signer to .none?
144198
case .none:
145-
let versionSigners = signerVersions.versionSigners
199+
let versionSigningEntities = packageSigners.versionSigningEntities
146200
// If the given version is semantically newer than any signed version,
147201
// then it must be signed. (i.e., when a package starts being signed
148202
// at a version, then all future versions must be signed.)
@@ -159,14 +213,19 @@ struct PackageSigningEntityTOFU {
159213
// a newer version (i.e., < 1.5.0) and we assume it to be signed.
160214
// - When unsigned v2.0.0 is downloaded, we don't fail because we haven't
161215
// seen a signed 2.x release yet, so we assume 2.x releases are not signed.
162-
let olderSignedVersions = versionSigners.keys.filter { $0.major == version.major && $0 < version }
216+
// (this might be controversial)
217+
let olderSignedVersions = versionSigningEntities.keys
218+
.filter { $0.major == version.major && $0 < version }
163219
.sorted(by: >)
164-
for signedVersion in olderSignedVersions {
165-
if let versionSigner = versionSigners[signedVersion] {
166-
return self.handleSigningEntityChanged(
220+
for olderSignedVersion in olderSignedVersions {
221+
if let olderVersionSigner = versionSigningEntities[olderSignedVersion]?.first {
222+
return self.handleSigningEntityForPackageChanged(
223+
registry: registry,
167224
package: package,
225+
version: version,
168226
latest: signingEntity,
169-
existing: versionSigner,
227+
existing: olderVersionSigner,
228+
existingVersion: olderSignedVersion,
170229
observabilityScope: observabilityScope
171230
) { result in
172231
completion(result.tryMap { false })
@@ -179,6 +238,7 @@ struct PackageSigningEntityTOFU {
179238
}
180239

181240
private func writeToStorage(
241+
registry: Registry,
182242
package: PackageIdentity.RegistryIdentity,
183243
version: Version,
184244
signingEntity: SigningEntity,
@@ -194,14 +254,16 @@ struct PackageSigningEntityTOFU {
194254
package: package.underlying,
195255
version: version,
196256
signingEntity: signingEntity,
257+
origin: .registry(registry.url),
197258
observabilityScope: observabilityScope,
198259
callbackQueue: callbackQueue
199260
) { result in
200261
switch result {
201262
case .success:
202263
completion(.success(()))
203264
case .failure(PackageSigningEntityStorageError.conflict(_, _, _, let existing)):
204-
self.handleSigningEntityChanged(
265+
self.handleSigningEntityForPackageVersionChanged(
266+
registry: registry,
205267
package: package,
206268
version: version,
207269
latest: signingEntity,
@@ -215,7 +277,8 @@ struct PackageSigningEntityTOFU {
215277
}
216278
}
217279

218-
private func handleSigningEntityChanged(
280+
private func handleSigningEntityForPackageVersionChanged(
281+
registry: Registry,
219282
package: PackageIdentity.RegistryIdentity,
220283
version: Version,
221284
latest: SigningEntity?,
@@ -226,6 +289,7 @@ struct PackageSigningEntityTOFU {
226289
switch self.signingEntityCheckingMode {
227290
case .strict:
228291
completion(.failure(RegistryError.signingEntityForReleaseChanged(
292+
registry: registry,
229293
package: package.underlying,
230294
version: version,
231295
latest: latest,
@@ -234,29 +298,36 @@ struct PackageSigningEntityTOFU {
234298
case .warn:
235299
observabilityScope
236300
.emit(
237-
warning: "The signing entity '\(String(describing: latest))' for \(package) version \(version) does not match previously recorded value '\(existing)'"
301+
warning: "the signing entity '\(String(describing: latest))' from \(registry) for \(package) version \(version) is different from the previously recorded value '\(existing)'"
238302
)
239303
completion(.success(()))
240304
}
241305
}
242306

243-
private func handleSigningEntityChanged(
307+
private func handleSigningEntityForPackageChanged(
308+
registry: Registry,
244309
package: PackageIdentity.RegistryIdentity,
310+
version: Version,
245311
latest: SigningEntity?,
246312
existing: SigningEntity,
313+
existingVersion: Version,
247314
observabilityScope: ObservabilityScope,
248315
completion: @escaping (Result<Void, Error>) -> Void
249316
) {
250317
switch self.signingEntityCheckingMode {
251318
case .strict:
252319
completion(.failure(RegistryError.signingEntityForPackageChanged(
320+
registry: registry,
253321
package: package.underlying,
254-
latest: latest, previous: existing
322+
version: version,
323+
latest: latest,
324+
previous: existing,
325+
previousVersion: existingVersion
255326
)))
256327
case .warn:
257328
observabilityScope
258329
.emit(
259-
warning: "The signing entity '\(String(describing: latest))' for \(package) does not match previously recorded value '\(existing)'"
330+
warning: "the signing entity '\(String(describing: latest))' from \(registry) for \(package) version \(version) is different from the previously recorded value '\(existing)' for version \(existingVersion)"
260331
)
261332
completion(.success(()))
262333
}

0 commit comments

Comments
 (0)