9
9
module dub.recipe.selection ;
10
10
11
11
import dub.dependency;
12
+ import dub.internal.vibecompat.data.json : Json;
12
13
import dub.internal.vibecompat.inet.path : NativePath;
13
14
14
15
import dub.internal.configy.attributes;
15
16
import dub.internal.dyaml.stdsumtype;
16
17
18
+ import std.algorithm.iteration : each;
19
+ import std.algorithm.searching : canFind;
17
20
import std.exception ;
21
+ import std.format : format;
22
+ import std.range : enumerate;
23
+ import std.string : indexOf;
18
24
19
25
deprecated (" Use either `Selections!1` or `SelectionsFile` instead" )
20
26
public alias Selected = Selections! 1 ;
@@ -125,21 +131,25 @@ public struct Selections (ushort Version)
125
131
126
132
// / Wrapper around `SelectedDependency` to do deserialization but still provide
127
133
// / a `Dependency` object to client code.
128
- private struct SelectedDependency
134
+ package (dub) struct SelectedDependency
129
135
{
130
136
public Dependency actual;
131
137
alias actual this ;
138
+ public IntegrityTag integrity;
132
139
133
140
// / Constructor, used in `fromConfig`
134
- public this (inout (Dependency) dep) inout @safe pure nothrow @nogc
141
+ public this (inout (Dependency) dep, const IntegrityTag tag = IntegrityTag.init)
142
+ inout @safe pure nothrow @nogc
135
143
{
136
144
this .actual = dep;
145
+ this .integrity = tag;
137
146
}
138
147
139
148
// / Allow external code to assign to this object as if it was a `Dependency`
140
149
public ref SelectedDependency opAssign (Dependency dep) return pure nothrow @nogc
141
150
{
142
151
this .actual = dep;
152
+ this .integrity = IntegrityTag.init;
143
153
return this ;
144
154
}
145
155
@@ -157,16 +167,41 @@ private struct SelectedDependency
157
167
assert (d.version_.length);
158
168
if (d.repository.length)
159
169
return SelectedDependency (Dependency(Repository(d.repository, d.version_)));
160
- return SelectedDependency (Dependency(Version(d.version_)));
170
+ return SelectedDependency (Dependency(Version(d.version_)), d.integrity );
161
171
}
162
172
}
163
173
174
+ // / Serializes a selected version to JSON for `dub.selections.json`
175
+ public Json toJsonDep () const {
176
+ version (none ) {
177
+ // The following is not yet enabled, because we're currently only
178
+ // able to get an integrity tag value when the package is first
179
+ // downloaded. This is problematic as most of the time, we try
180
+ // to reuse packages, and most common use of `dub upgrade` would
181
+ // make the integrity tag flip between empty or not.
182
+ // However, with this code enabled, one may get an integrity tag
183
+ // written to their `dub.selections.json` under two conditions:
184
+ // 1) The package is not present on the file system;
185
+ // 2) The package is upgraded (e.g. `dub upgrade` would normally trigger);
186
+ if (this .integrity.value.length && this .actual.isExactVersion()) {
187
+ const vers = this .actual.version_();
188
+ Json result = Json.emptyObject;
189
+ result[" version" ] = Json(vers.toString());
190
+ result[" integrity" ] = Json(
191
+ " %s-%s" .format(this .integrity.algorithm, this .integrity.value));
192
+ return result;
193
+ }
194
+ }
195
+ return this .actual.toJson(true );
196
+ }
197
+
164
198
// / In-file representation of a dependency as permitted in `dub.selections.json`
165
199
private struct YAMLFormat
166
200
{
167
201
@Optional @Name(" version" ) string version_;
168
202
@Optional string path;
169
203
@Optional string repository;
204
+ @Optional IntegrityTag integrity;
170
205
171
206
public void validate () const scope @safe pure
172
207
{
@@ -178,10 +213,134 @@ private struct SelectedDependency
178
213
" Cannot provide a `path` dependency if a `version` dependency is used" );
179
214
enforce(! this .repository.length || this .version_.length,
180
215
" Cannot provide a `repository` dependency without a `version`" );
216
+ enforce(! this .integrity.algorithm.length || (! this .path.length && ! this .repository.length),
217
+ " `integrity` property is only supported for `version` dependencies" );
181
218
}
182
219
}
183
220
}
184
221
222
+ /**
223
+ * A subresource integrity declaration
224
+ *
225
+ * Implement the SRI (Subresource Integrity) standard, used to validate that
226
+ * a given dependency is of the expected version.
227
+ *
228
+ * One may get an integrity tag in base64 using openssl:
229
+ * ```
230
+ * $ cat vibe.d-0.10.1.zip | openssl dgst -binary -sha512 | base64
231
+ * vwQ9tYTjLb981j41+3GZZUgKXm/5PlKpmY2bplRSUM8ajL03++LGm/TcfFFarJrHex8CTb5ZLWdi
232
+ * Y1fFAOSkSw==
233
+ * ```
234
+ *
235
+ * See_Also:
236
+ * https://w3c.github.io/webappsec-subresource-integrity/#the-integrity-attribute
237
+ */
238
+ public struct IntegrityTag
239
+ {
240
+ // / The hash function to use
241
+ public string algorithm;
242
+ // / The value of the digest computed with `algorithm`, base64-encoded
243
+ public string value;
244
+
245
+ // / Parses a string representation as an `IntegrityTag`
246
+ public this (string value)
247
+ {
248
+ auto sep = indexOf(value, ' -' );
249
+ enforce(sep > 0 , ` Expected a string in the form 'hash-algorithm "-" base64-value', e.g. 'sha512-...'` );
250
+ this .algorithm = value[0 .. sep];
251
+ this .value = value[sep + 1 .. $];
252
+ switch (this .algorithm) {
253
+ case " sha512" :
254
+ enforce(this .value.length == 88 ,
255
+ " Excepted a base64-encoded sha512 digest of 88 characters, not %s"
256
+ .format(this .value.length));
257
+ break ;
258
+ case " sha384" :
259
+ enforce(this .value.length == 64 ,
260
+ " Excepted a base64-encoded sha384 digest of 64 characters, not %s"
261
+ .format(this .value.length));
262
+ break ;
263
+ case " sha256" :
264
+ enforce(this .value.length == 40 ,
265
+ " Excepted a base64-encoded sha256 digest of 40 characters, not %s"
266
+ .format(this .value.length));
267
+ break ;
268
+ default :
269
+ throw new Exception (" Algorithm '" ~ this .algorithm ~
270
+ " ' is not supported, expected one of: 'sha512', 'sha384', 'sha256'" );
271
+ }
272
+ this .value.enumerate.each! ((size_t idx, dchar c) {
273
+ enforce(" ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" .canFind(c),
274
+ " Expected digest to be base64 encoded, found non-base64 character '%s' at index '%s'"
275
+ .format(c, idx));
276
+ });
277
+ }
278
+
279
+ // / Internal constructor for `IntegrityTag.make`
280
+ public this (string algorithm, string value) inout @safe pure nothrow @nogc {
281
+ this .algorithm = algorithm;
282
+ this .value = value;
283
+ }
284
+
285
+ /**
286
+ * Verify if the data passed in parameter matches this `IntegrityTag`
287
+ *
288
+ * Params:
289
+ * data = The content of the archive to check for a match
290
+ *
291
+ * Returns:
292
+ * Whether the hash of `data` (using `this.algorithm)` matches
293
+ * the value that is `base64` encoded.
294
+ */
295
+ public bool matches (in ubyte [] data) const @safe pure {
296
+ import std.base64 ;
297
+ import std.digest.sha ;
298
+
299
+ ubyte [64 ] buffer; // 32, 48, or 64 bytes used
300
+ auto decoded = Base64.decode(this .value, buffer[]);
301
+ switch (this .algorithm) {
302
+ case " sha512" :
303
+ return sha512Of (data) == decoded;
304
+ case " sha384" :
305
+ return sha384Of (data) == decoded;
306
+ case " sha256" :
307
+ return sha256Of (data) == decoded;
308
+ default :
309
+ assert (0 , " An `IntegrityTag` with non-supported algorithm was created: " ~ this .algorithm);
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Build and returns an `IntegrityTag`
315
+ *
316
+ * This is a convenience function to build an `IntegrityTag` from the
317
+ * archive data. Use sha512 by default.
318
+ *
319
+ * Params:
320
+ * data = The content of the archive to check hash into a digest
321
+ * algorithm = One of `sha256`, `sha384`, `sha512`. Default to the latter.
322
+ *
323
+ * Returns:
324
+ * A populated `IntegrityTag`.
325
+ */
326
+ public static IntegrityTag make (in ubyte [] data, string algorithm = " sha512" )
327
+ @safe pure {
328
+ import std.base64 ;
329
+ import std.digest.sha ;
330
+
331
+ switch (algorithm) {
332
+ case " sha512" :
333
+ return IntegrityTag (algorithm, Base64.encode(sha512Of(data)));
334
+ case " sha384" :
335
+ return IntegrityTag (algorithm, Base64.encode(sha384Of(data)));
336
+ case " sha256" :
337
+ return IntegrityTag (algorithm, Base64.encode(sha256Of(data)));
338
+ default :
339
+ assert (0 , " `IntegrityTag.make` was called with non-supported algorithm: " ~ algorithm);
340
+ }
341
+ }
342
+ }
343
+
185
344
// Ensure we can read all type of dependencies
186
345
unittest
187
346
{
@@ -191,6 +350,10 @@ unittest
191
350
"fileVersion": 1,
192
351
"versions": {
193
352
"simple": "1.5.6",
353
+ "complex": { "version": "1.2.3" },
354
+ "digest": { "version": "1.2.3", "integrity": "sha256-abcdefghijklmnopqrstuvwxyz0123456789+/==" },
355
+ "digest1": { "version": "1.2.3", "integrity": "sha384-Li9vy3DqF8tnTXuiaAJuML3ky+er10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7" },
356
+ "digest2": { "version": "1.2.3", "integrity": "sha512-Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp3WoNr3bjZSAHeOsHrbV1Fu9/A0EzCinRE7Af1ofPrw==" },
194
357
"branch": "~master",
195
358
"branch2": "~main",
196
359
"path": { "path": "../some/where" },
@@ -205,8 +368,12 @@ unittest
205
368
(s) { assert (0 ); return Selections! (1 ).init; },
206
369
);
207
370
assert (! s.inheritable);
208
- assert (s.versions.length == 5 );
371
+ assert (s.versions.length == 9 );
209
372
assert (s.versions[" simple" ] == Dependency(Version(" 1.5.6" )));
373
+ assert (s.versions[" complex" ] == Dependency(Version(" 1.2.3" )));
374
+ assert (s.versions[" digest" ] == Dependency(Version(" 1.2.3" )));
375
+ assert (s.versions[" digest1" ] == Dependency(Version(" 1.2.3" )));
376
+ assert (s.versions[" digest2" ] == Dependency(Version(" 1.2.3" )));
210
377
assert (s.versions[" branch" ] == Dependency(Version(" ~master" )));
211
378
assert (s.versions[" branch2" ] == Dependency(Version(" ~main" )));
212
379
assert (s.versions[" path" ] == Dependency(NativePath(" ../some/where" )));
0 commit comments