@@ -31,6 +31,7 @@ struct PackageSigningEntityTOFU {
31
31
}
32
32
33
33
func validate(
34
+ registry: Registry ,
34
35
package : PackageIdentity . RegistryIdentity ,
35
36
version: Version ,
36
37
signingEntity: SigningEntity ? ,
@@ -48,12 +49,13 @@ struct PackageSigningEntityTOFU {
48
49
callbackQueue: callbackQueue
49
50
) { result in
50
51
switch result {
51
- case . success( let signerVersions ) :
52
+ case . success( let packageSigners ) :
52
53
self . validateSigningEntity (
54
+ registry: registry,
53
55
package : package ,
54
56
version: version,
55
57
signingEntity: signingEntity,
56
- signerVersions : signerVersions ,
58
+ packageSigners : packageSigners ,
57
59
observabilityScope: observabilityScope
58
60
) { validateResult in
59
61
switch validateResult {
@@ -63,6 +65,7 @@ struct PackageSigningEntityTOFU {
63
65
return completion ( . success( ( ) ) )
64
66
}
65
67
self . writeToStorage (
68
+ registry: registry,
66
69
package : package ,
67
70
version: version,
68
71
signingEntity: signingEntity,
@@ -82,36 +85,40 @@ struct PackageSigningEntityTOFU {
82
85
}
83
86
84
87
private func validateSigningEntity(
88
+ registry: Registry ,
85
89
package : PackageIdentity . RegistryIdentity ,
86
90
version: Version ,
87
91
signingEntity: SigningEntity ? ,
88
- signerVersions : [ SigningEntity : Set < Version > ] ,
92
+ packageSigners : PackageSigners ,
89
93
observabilityScope: ObservabilityScope ,
90
94
completion: @escaping ( Result < Bool , Error > ) -> Void
91
95
) {
92
96
// Package is never signed.
93
97
// 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 {
96
100
return completion ( . success( true ) )
97
101
}
98
102
99
103
// If we get to this point, it means we have seen a signed version of the package.
100
104
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,
111
118
package : package ,
112
119
version: version,
113
120
latest: signingEntity,
114
- existing: signerForVersion ,
121
+ existing: signingEntitiesForVersion . first! , // !-safe since signingEntitiesForVersion is non-empty
115
122
observabilityScope: observabilityScope
116
123
) { result in
117
124
completion ( result. tryMap { false } )
@@ -121,28 +128,75 @@ struct PackageSigningEntityTOFU {
121
128
return completion ( . success( false ) )
122
129
}
123
130
131
+ // Check signer(s) of other version(s)
124
132
switch signingEntity {
125
133
// Is the package changing from one signer to another?
126
134
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,
132
160
package : package ,
161
+ version: version,
133
162
latest: signingEntity,
134
- existing: otherSigner,
163
+ existing: expectedSigner. signingEntity,
164
+ existingVersion: expectedSigner. fromVersion,
135
165
observabilityScope: observabilityScope
136
166
) { result in
137
167
completion ( result. tryMap { false } )
138
168
}
139
169
} 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
+
140
194
// Package doesn't have any other signer besides the given one, which is good.
141
195
completion ( . success( true ) )
142
196
}
143
197
// Or is the package going from having a signer to .none?
144
198
case . none:
145
- let versionSigners = signerVersions . versionSigners
199
+ let versionSigningEntities = packageSigners . versionSigningEntities
146
200
// If the given version is semantically newer than any signed version,
147
201
// then it must be signed. (i.e., when a package starts being signed
148
202
// at a version, then all future versions must be signed.)
@@ -159,14 +213,19 @@ struct PackageSigningEntityTOFU {
159
213
// a newer version (i.e., < 1.5.0) and we assume it to be signed.
160
214
// - When unsigned v2.0.0 is downloaded, we don't fail because we haven't
161
215
// 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 }
163
219
. 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,
167
224
package : package ,
225
+ version: version,
168
226
latest: signingEntity,
169
- existing: versionSigner,
227
+ existing: olderVersionSigner,
228
+ existingVersion: olderSignedVersion,
170
229
observabilityScope: observabilityScope
171
230
) { result in
172
231
completion ( result. tryMap { false } )
@@ -179,6 +238,7 @@ struct PackageSigningEntityTOFU {
179
238
}
180
239
181
240
private func writeToStorage(
241
+ registry: Registry ,
182
242
package : PackageIdentity . RegistryIdentity ,
183
243
version: Version ,
184
244
signingEntity: SigningEntity ,
@@ -194,14 +254,16 @@ struct PackageSigningEntityTOFU {
194
254
package : package . underlying,
195
255
version: version,
196
256
signingEntity: signingEntity,
257
+ origin: . registry( registry. url) ,
197
258
observabilityScope: observabilityScope,
198
259
callbackQueue: callbackQueue
199
260
) { result in
200
261
switch result {
201
262
case . success:
202
263
completion ( . success( ( ) ) )
203
264
case . failure( PackageSigningEntityStorageError . conflict( _, _, _, let existing) ) :
204
- self . handleSigningEntityChanged (
265
+ self . handleSigningEntityForPackageVersionChanged (
266
+ registry: registry,
205
267
package : package ,
206
268
version: version,
207
269
latest: signingEntity,
@@ -215,7 +277,8 @@ struct PackageSigningEntityTOFU {
215
277
}
216
278
}
217
279
218
- private func handleSigningEntityChanged(
280
+ private func handleSigningEntityForPackageVersionChanged(
281
+ registry: Registry ,
219
282
package : PackageIdentity . RegistryIdentity ,
220
283
version: Version ,
221
284
latest: SigningEntity ? ,
@@ -226,6 +289,7 @@ struct PackageSigningEntityTOFU {
226
289
switch self . signingEntityCheckingMode {
227
290
case . strict:
228
291
completion ( . failure( RegistryError . signingEntityForReleaseChanged (
292
+ registry: registry,
229
293
package : package . underlying,
230
294
version: version,
231
295
latest: latest,
@@ -234,29 +298,36 @@ struct PackageSigningEntityTOFU {
234
298
case . warn:
235
299
observabilityScope
236
300
. 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) ' "
238
302
)
239
303
completion ( . success( ( ) ) )
240
304
}
241
305
}
242
306
243
- private func handleSigningEntityChanged(
307
+ private func handleSigningEntityForPackageChanged(
308
+ registry: Registry ,
244
309
package : PackageIdentity . RegistryIdentity ,
310
+ version: Version ,
245
311
latest: SigningEntity ? ,
246
312
existing: SigningEntity ,
313
+ existingVersion: Version ,
247
314
observabilityScope: ObservabilityScope ,
248
315
completion: @escaping ( Result < Void , Error > ) -> Void
249
316
) {
250
317
switch self . signingEntityCheckingMode {
251
318
case . strict:
252
319
completion ( . failure( RegistryError . signingEntityForPackageChanged (
320
+ registry: registry,
253
321
package : package . underlying,
254
- latest: latest, previous: existing
322
+ version: version,
323
+ latest: latest,
324
+ previous: existing,
325
+ previousVersion: existingVersion
255
326
) ) )
256
327
case . warn:
257
328
observabilityScope
258
329
. 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 ) "
260
331
)
261
332
completion ( . success( ( ) ) )
262
333
}
0 commit comments