fiftyone.pipeline.did 4.5.37 → 4.5.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/didClient.js +5 -4
- package/examples/fodIdExample.js +20 -10
- package/fodId.js +48 -80
- package/index.js +2 -0
- package/internal/layout.js +62 -0
- package/node_modules/owid/README.md +12 -0
- package/node_modules/owid/owid.crypto.test.js +85 -5
- package/node_modules/owid/owid.interop.test.js +20 -3
- package/node_modules/owid/owid.published-key.test.js +253 -0
- package/node_modules/owid/owid.test.js +6 -1
- package/node_modules/owid/testdata/51did-identifier.json +6 -0
- package/node_modules/owid/testdata/51did-public-keys.json +182 -0
- package/node_modules/owid/v1.js +34 -3
- package/package.json +10 -3
- package/readme.md +72 -18
- package/tests/didClient.integration.test.js +1 -1
- package/tests/didClient.test.js +6 -5
- package/tests/envelope.js +13 -13
- package/tests/fodId.test.js +112 -87
- package/types/didClient.d.ts +17 -2
- package/types/fodId.d.ts +28 -58
- package/types/index.d.ts +2 -1
- package/types/internal/layout.d.ts +26 -0
- package/types/usage.d.ts +34 -0
- package/usage.js +89 -0
package/didClient.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* ********************************************************************* */
|
|
22
22
|
|
|
23
23
|
const FodId = require('./fodId');
|
|
24
|
+
const layout = require('./internal/layout');
|
|
24
25
|
const IdType = require('./idType');
|
|
25
26
|
const packageVersion = require('./package.json').version;
|
|
26
27
|
|
|
@@ -777,7 +778,7 @@ function ensureEncodedLength (value) {
|
|
|
777
778
|
* @returns {Date} the moment the envelope says it was created
|
|
778
779
|
*/
|
|
779
780
|
function dateOf (fodId) {
|
|
780
|
-
return new Date(OWID_EPOCH_MS + fodId.
|
|
781
|
+
return new Date(OWID_EPOCH_MS + fodId.date * MINUTE_MS);
|
|
781
782
|
}
|
|
782
783
|
|
|
783
784
|
/**
|
|
@@ -794,9 +795,9 @@ function dateOf (fodId) {
|
|
|
794
795
|
*/
|
|
795
796
|
function payloadLengthValid (fodId) {
|
|
796
797
|
const matchKeyLength = fodId.type === IdType.RANDOM
|
|
797
|
-
?
|
|
798
|
-
:
|
|
799
|
-
return fodId.payload.length >=
|
|
798
|
+
? layout.GUID_LENGTH
|
|
799
|
+
: layout.MATCH_KEY_LENGTH;
|
|
800
|
+
return fodId.payload.length >= layout.HEADER_LENGTH + matchKeyLength;
|
|
800
801
|
}
|
|
801
802
|
|
|
802
803
|
/**
|
package/examples/fodIdExample.js
CHANGED
|
@@ -33,7 +33,12 @@
|
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
35
|
const { webcrypto } = require('crypto');
|
|
36
|
-
const { FodId, IdType } = require('../index');
|
|
36
|
+
const { FodId, IdType, Usage } = require('../index');
|
|
37
|
+
// The payload byte layout is internal to the package, so a consumer
|
|
38
|
+
// never reaches for it. This example builds a sample 51Did to work on,
|
|
39
|
+
// which the cloud would otherwise issue, so it reads the layout from
|
|
40
|
+
// inside the package.
|
|
41
|
+
const layout = require('../internal/layout');
|
|
37
42
|
|
|
38
43
|
const subtle = webcrypto.subtle;
|
|
39
44
|
const VERSION = 2;
|
|
@@ -45,13 +50,17 @@ function uint32LE (v) {
|
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
function samplePayload () {
|
|
48
|
-
const p = new Uint8Array(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
p[
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
const p = new Uint8Array(layout.PAYLOAD_LENGTH);
|
|
54
|
+
// Bits 6 and 7 are zero, so the type is Probabilistic. Bits 0 to 2 are
|
|
55
|
+
// the usage and they are cumulative, so 0b011 grants standard
|
|
56
|
+
// marketing and, with it, non-marketing use.
|
|
57
|
+
p[layout.FLAGS_OFFSET] = 0b0000_0011;
|
|
58
|
+
p[layout.LICENSE_ID_OFFSET] = 0x78;
|
|
59
|
+
p[layout.LICENSE_ID_OFFSET + 1] = 0x56;
|
|
60
|
+
p[layout.LICENSE_ID_OFFSET + 2] = 0x34;
|
|
61
|
+
p[layout.LICENSE_ID_OFFSET + 3] = 0x12;
|
|
62
|
+
for (let i = 0; i < layout.MATCH_KEY_LENGTH; i++) {
|
|
63
|
+
p[layout.MATCH_KEY_OFFSET + i] = 0x20 + i;
|
|
55
64
|
}
|
|
56
65
|
return p;
|
|
57
66
|
}
|
|
@@ -94,7 +103,8 @@ async function run () {
|
|
|
94
103
|
console.log('51Did parsed from base64:');
|
|
95
104
|
console.log(' Domain :', fodId.domain);
|
|
96
105
|
console.log(' Type :', IdType.name(fodId.type));
|
|
97
|
-
console.log('
|
|
106
|
+
console.log(' Usage :', Usage.name(fodId.usage));
|
|
107
|
+
console.log(' From cons.:', fodId.usageFromConsent);
|
|
98
108
|
console.log(' LicenseId :', fodId.licenseId);
|
|
99
109
|
console.log(' Match key :', Buffer.from(fodId.matchKey).toString('hex'));
|
|
100
110
|
console.log(' Verifies :', await fodId.verify(publicPem));
|
|
@@ -129,7 +139,7 @@ async function run () {
|
|
|
129
139
|
', status ' + result.status);
|
|
130
140
|
}
|
|
131
141
|
const shortPayload = await issue(
|
|
132
|
-
keyPair.privateKey, payload.slice(0,
|
|
142
|
+
keyPair.privateKey, payload.slice(0, layout.PAYLOAD_LENGTH - 1), DATE);
|
|
133
143
|
const short = FodId.tryParse(shortPayload);
|
|
134
144
|
console.log(' a payload one byte short -> ok ' + short.ok +
|
|
135
145
|
', status ' + short.status);
|
package/fodId.js
CHANGED
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
* ********************************************************************* */
|
|
22
22
|
|
|
23
23
|
const owid = require('owid');
|
|
24
|
+
const layout = require('./internal/layout');
|
|
24
25
|
const IdType = require('./idType');
|
|
26
|
+
const Usage = require('./usage');
|
|
25
27
|
const FodIdParseError = require('./fodIdParseError');
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -82,42 +84,17 @@ const ParseStatus = Object.freeze(Object.assign({}, owid.ParseStatus, {
|
|
|
82
84
|
* A FodId composes the OWID the OWID library read (holds it and delegates
|
|
83
85
|
* the envelope fields to it). That OWID is frozen and hands out its byte
|
|
84
86
|
* arrays as copies, so nothing a caller holds can change the identifier.
|
|
87
|
+
*
|
|
88
|
+
* Every field has a named accessor here, so nothing needs the payload
|
|
89
|
+
* bytes or their offsets. The byte layout itself is specified once for all
|
|
90
|
+
* languages, and that specification is the authority rather than this
|
|
91
|
+
* comment:
|
|
92
|
+
* https://github.com/51Degrees/specifications/blob/main/did-specification/identifier-layout.md
|
|
93
|
+
*
|
|
94
|
+
* What each 51Did package offers on top of that layout is specified at:
|
|
95
|
+
* https://github.com/51Degrees/specifications/blob/main/did-specification/package-surface.md
|
|
85
96
|
*/
|
|
86
97
|
class FodId {
|
|
87
|
-
static FLAGS_OFFSET = 0;
|
|
88
|
-
static LICENSE_ID_OFFSET = 1;
|
|
89
|
-
static LICENSE_ID_LENGTH = 4;
|
|
90
|
-
/** Byte offset of the match key field within the payload. */
|
|
91
|
-
static MATCH_KEY_OFFSET = 5;
|
|
92
|
-
/**
|
|
93
|
-
* Byte length of the match key field for Probabilistic and HashedEmail
|
|
94
|
-
* identifiers, being a SHA-256.
|
|
95
|
-
*/
|
|
96
|
-
static MATCH_KEY_LENGTH = 32;
|
|
97
|
-
/**
|
|
98
|
-
* Deprecated alias for {@link FodId.MATCH_KEY_OFFSET}. The stable,
|
|
99
|
-
* comparable part of a 51Did is now called the match key, mirroring the
|
|
100
|
-
* Model Terms for Marketing vocabulary. This alias will be removed in a
|
|
101
|
-
* future release.
|
|
102
|
-
* @deprecated Renamed to MATCH_KEY_OFFSET. This alias will be removed in
|
|
103
|
-
* a future release.
|
|
104
|
-
*/
|
|
105
|
-
static HASH_OFFSET = FodId.MATCH_KEY_OFFSET;
|
|
106
|
-
/**
|
|
107
|
-
* Deprecated alias for {@link FodId.MATCH_KEY_LENGTH}. The stable,
|
|
108
|
-
* comparable part of a 51Did is now called the match key, mirroring the
|
|
109
|
-
* Model Terms for Marketing vocabulary. This alias will be removed in a
|
|
110
|
-
* future release.
|
|
111
|
-
* @deprecated Renamed to MATCH_KEY_LENGTH. This alias will be removed in
|
|
112
|
-
* a future release.
|
|
113
|
-
*/
|
|
114
|
-
static HASH_LENGTH = FodId.MATCH_KEY_LENGTH;
|
|
115
|
-
static HEADER_LENGTH = FodId.MATCH_KEY_OFFSET;
|
|
116
|
-
/** Byte length of the GUID match key carried by Random identifiers. */
|
|
117
|
-
static GUID_LENGTH = 16;
|
|
118
|
-
static RANDOM_PAYLOAD_LENGTH = 21;
|
|
119
|
-
static PAYLOAD_LENGTH = FodId.MATCH_KEY_OFFSET + FodId.MATCH_KEY_LENGTH;
|
|
120
|
-
|
|
121
98
|
/**
|
|
122
99
|
* Why a read succeeded or failed, being the OWID library's statuses plus
|
|
123
100
|
* `PAYLOAD_TOO_SHORT` and `INVALID_TYPE_PAYLOAD_LENGTH`. Frozen.
|
|
@@ -303,16 +280,31 @@ class FodId {
|
|
|
303
280
|
return new FodId(owidInstance);
|
|
304
281
|
}
|
|
305
282
|
|
|
306
|
-
/** @returns {number} the 1-byte usage flags bit-mask (0-255). */
|
|
307
|
-
get flags () {
|
|
308
|
-
return this._flags;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
283
|
/** @returns {number} the IdType carried in bits 6-7 of the flags. */
|
|
312
284
|
get type () {
|
|
313
285
|
return IdType.fromFlags(this._flags);
|
|
314
286
|
}
|
|
315
287
|
|
|
288
|
+
/**
|
|
289
|
+
* The Usage carried in bits 0-2 of the flags, as the highest usage
|
|
290
|
+
* granted. See Usage for why it is read that way.
|
|
291
|
+
* @returns {number} a Usage value
|
|
292
|
+
*/
|
|
293
|
+
get usage () {
|
|
294
|
+
return Usage.fromFlags(this._flags);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Whether the usage was derived from an IAB consent string the caller
|
|
299
|
+
* sent, rather than stated by the caller directly. Bit 3 of the flags.
|
|
300
|
+
* Both are legitimate ways to arrive at a usage, and this says nothing
|
|
301
|
+
* about which usage it is.
|
|
302
|
+
* @returns {boolean}
|
|
303
|
+
*/
|
|
304
|
+
get usageFromConsent () {
|
|
305
|
+
return (this._flags & 0b1000) !== 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
316
308
|
/**
|
|
317
309
|
* The 4-byte little-endian field at offset 1 of the payload, as an
|
|
318
310
|
* unsigned integer (0-4294967295).
|
|
@@ -341,19 +333,6 @@ class FodId {
|
|
|
341
333
|
return this._matchKey.slice();
|
|
342
334
|
}
|
|
343
335
|
|
|
344
|
-
/**
|
|
345
|
-
* Deprecated alias for {@link FodId#matchKey}. The stable, comparable
|
|
346
|
-
* part of a 51Did is now called the match key, mirroring the Model Terms
|
|
347
|
-
* for Marketing vocabulary. This alias will be removed in a future
|
|
348
|
-
* release.
|
|
349
|
-
* @deprecated Renamed to matchKey. This alias will be removed in a future
|
|
350
|
-
* release.
|
|
351
|
-
* @returns {Uint8Array} the same bytes as {@link FodId#matchKey}
|
|
352
|
-
*/
|
|
353
|
-
get hash () {
|
|
354
|
-
return this.matchKey;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
336
|
/** @returns {number} the OWID version. */
|
|
358
337
|
get version () {
|
|
359
338
|
return this._owid.version;
|
|
@@ -365,24 +344,13 @@ class FodId {
|
|
|
365
344
|
}
|
|
366
345
|
|
|
367
346
|
/**
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
return this._owid.date;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* The envelope's own date as the unsigned 32-bit count of minutes since
|
|
377
|
-
* 2020-01-01T00:00:00Z, exactly as the wire carries it. This is the value
|
|
378
|
-
* the OWID `public-key?date=` parameter takes, and the integer to use when
|
|
379
|
-
* comparing creation times. The OWID library now reads the field unsigned
|
|
380
|
-
* too, so {@link FodId#date} agrees with this getter. The getter is kept
|
|
381
|
-
* because callers were told to use it, and it still forces the unsigned
|
|
382
|
-
* reading should the field ever arrive signed.
|
|
347
|
+
* The envelope's own date, being the count of minutes since
|
|
348
|
+
* 2020-01-01T00:00:00Z exactly as the wire carries it, read as an
|
|
349
|
+
* unsigned 32-bit number. This is the integer to compare when asking
|
|
350
|
+
* which of two identifiers was issued first.
|
|
383
351
|
* @returns {number} minutes since 2020-01-01T00:00:00Z
|
|
384
352
|
*/
|
|
385
|
-
get
|
|
353
|
+
get date () {
|
|
386
354
|
return this._owid.date >>> 0;
|
|
387
355
|
}
|
|
388
356
|
|
|
@@ -464,34 +432,34 @@ class FodId {
|
|
|
464
432
|
*/
|
|
465
433
|
function unpack (payload) {
|
|
466
434
|
const length = payload.length;
|
|
467
|
-
if (length <
|
|
435
|
+
if (length < layout.HEADER_LENGTH) {
|
|
468
436
|
return {
|
|
469
437
|
status: ParseStatus.PAYLOAD_TOO_SHORT,
|
|
470
438
|
length,
|
|
471
|
-
required:
|
|
439
|
+
required: layout.HEADER_LENGTH
|
|
472
440
|
};
|
|
473
441
|
}
|
|
474
|
-
const flags = payload[
|
|
442
|
+
const flags = payload[layout.FLAGS_OFFSET];
|
|
475
443
|
// Little-endian unsigned 32-bit. `>>> 0` forces unsigned so the high bit
|
|
476
444
|
// does not produce a negative number.
|
|
477
445
|
const licenseId = (
|
|
478
|
-
payload[
|
|
479
|
-
(payload[
|
|
480
|
-
(payload[
|
|
481
|
-
(payload[
|
|
446
|
+
payload[layout.LICENSE_ID_OFFSET] |
|
|
447
|
+
(payload[layout.LICENSE_ID_OFFSET + 1] << 8) |
|
|
448
|
+
(payload[layout.LICENSE_ID_OFFSET + 2] << 16) |
|
|
449
|
+
(payload[layout.LICENSE_ID_OFFSET + 3] << 24)
|
|
482
450
|
) >>> 0;
|
|
483
451
|
const type = IdType.fromFlags(flags);
|
|
484
452
|
let matchKeyLength;
|
|
485
453
|
if (type === IdType.RANDOM) {
|
|
486
|
-
matchKeyLength =
|
|
454
|
+
matchKeyLength = layout.GUID_LENGTH;
|
|
487
455
|
} else if (type === IdType.RESERVED) {
|
|
488
456
|
// Not yet assigned, so read best-effort, whatever follows the header
|
|
489
457
|
// is the match key.
|
|
490
|
-
matchKeyLength = length -
|
|
458
|
+
matchKeyLength = length - layout.HEADER_LENGTH;
|
|
491
459
|
} else {
|
|
492
|
-
matchKeyLength =
|
|
460
|
+
matchKeyLength = layout.MATCH_KEY_LENGTH;
|
|
493
461
|
}
|
|
494
|
-
const required =
|
|
462
|
+
const required = layout.HEADER_LENGTH + matchKeyLength;
|
|
495
463
|
if (length < required) {
|
|
496
464
|
return {
|
|
497
465
|
status: ParseStatus.INVALID_TYPE_PAYLOAD_LENGTH,
|
|
@@ -506,7 +474,7 @@ function unpack (payload) {
|
|
|
506
474
|
licenseId,
|
|
507
475
|
// slice() copies, so the stored match key is this identifier's own.
|
|
508
476
|
matchKey: payload.slice(
|
|
509
|
-
|
|
477
|
+
layout.MATCH_KEY_OFFSET, layout.MATCH_KEY_OFFSET + matchKeyLength),
|
|
510
478
|
length,
|
|
511
479
|
required
|
|
512
480
|
};
|
package/index.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
const FodId = require('./fodId');
|
|
24
24
|
const FodIdParseError = require('./fodIdParseError');
|
|
25
25
|
const IdType = require('./idType');
|
|
26
|
+
const Usage = require('./usage');
|
|
26
27
|
const {
|
|
27
28
|
DidClient,
|
|
28
29
|
RedeemResult,
|
|
@@ -39,6 +40,7 @@ module.exports = {
|
|
|
39
40
|
FodId,
|
|
40
41
|
FodIdParseError,
|
|
41
42
|
IdType,
|
|
43
|
+
Usage,
|
|
42
44
|
DidClient,
|
|
43
45
|
RedeemResult,
|
|
44
46
|
ContextResult,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/* *********************************************************************
|
|
2
|
+
* This Original Work is copyright of 51 Degrees Mobile Experts Limited.
|
|
3
|
+
* Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House,
|
|
4
|
+
* Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU.
|
|
5
|
+
*
|
|
6
|
+
* This Original Work is licensed under the European Union Public Licence
|
|
7
|
+
* (EUPL) v.1.2 and is subject to its terms as set out below.
|
|
8
|
+
*
|
|
9
|
+
* If a copy of the EUPL was not distributed with this file, You can obtain
|
|
10
|
+
* one at https://opensource.org/licenses/EUPL-1.2.
|
|
11
|
+
*
|
|
12
|
+
* The 'Compatible Licences' set out in the Appendix to the EUPL (as may be
|
|
13
|
+
* amended by the European Commission) shall be deemed incompatible for
|
|
14
|
+
* the purposes of the Work and the provisions of the compatibility
|
|
15
|
+
* clause in Article 5 of the EUPL shall not apply.
|
|
16
|
+
*
|
|
17
|
+
* If using the Work as, or as part of, a network application, by
|
|
18
|
+
* including the attribution notice(s) required under Article 5 of the EUPL
|
|
19
|
+
* in the end user terms of the application under an appropriate heading,
|
|
20
|
+
* such notice(s) shall fulfill the requirements of that article.
|
|
21
|
+
* ********************************************************************* */
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The byte layout of a 51Did payload. Internal to this package, because the
|
|
25
|
+
* only reason to read the payload by hand is to read a field that already
|
|
26
|
+
* has a name on FodId, and reading the flags byte by hand is how the usage
|
|
27
|
+
* comes out backwards. The usage bits are cumulative, so a caller masking
|
|
28
|
+
* for the non-marketing bit alone reads every marketing identifier as
|
|
29
|
+
* non-marketing. Use the FodId accessors instead.
|
|
30
|
+
*
|
|
31
|
+
* The layout is described once for every language in the 51Did
|
|
32
|
+
* specification, which is the authority for these numbers:
|
|
33
|
+
* https://github.com/51Degrees/specifications/blob/main/did-specification/identifier-layout.md
|
|
34
|
+
*
|
|
35
|
+
* What each package offers on top of that layout is described in:
|
|
36
|
+
* https://github.com/51Degrees/specifications/blob/main/did-specification/package-surface.md
|
|
37
|
+
*/
|
|
38
|
+
module.exports = Object.freeze({
|
|
39
|
+
/** Byte offset of the flags byte within the payload. */
|
|
40
|
+
FLAGS_OFFSET: 0,
|
|
41
|
+
/** Byte offset of the licence id field within the payload. */
|
|
42
|
+
LICENSE_ID_OFFSET: 1,
|
|
43
|
+
/** Byte length of the licence id field. */
|
|
44
|
+
LICENSE_ID_LENGTH: 4,
|
|
45
|
+
/** Byte offset of the match key field within the payload. */
|
|
46
|
+
MATCH_KEY_OFFSET: 5,
|
|
47
|
+
/**
|
|
48
|
+
* Byte length of the match key field for Probabilistic and HashedEmail
|
|
49
|
+
* identifiers, being a SHA-256.
|
|
50
|
+
*/
|
|
51
|
+
MATCH_KEY_LENGTH: 32,
|
|
52
|
+
/** Byte length of the flags and licence id fields together. */
|
|
53
|
+
HEADER_LENGTH: 5,
|
|
54
|
+
/** Byte length of the GUID match key carried by Random identifiers. */
|
|
55
|
+
GUID_LENGTH: 16,
|
|
56
|
+
/** Least payload length for a Random identifier. */
|
|
57
|
+
RANDOM_PAYLOAD_LENGTH: 21,
|
|
58
|
+
/**
|
|
59
|
+
* Least payload length for a Probabilistic or HashedEmail identifier.
|
|
60
|
+
*/
|
|
61
|
+
PAYLOAD_LENGTH: 37
|
|
62
|
+
});
|
|
@@ -22,6 +22,12 @@ public key from their well known end point and verifies the ECDSA signature
|
|
|
22
22
|
locally. When `crypto.subtle` is not available it falls back to the creator's
|
|
23
23
|
remote verify end point.
|
|
24
24
|
|
|
25
|
+
Both end points are versioned, `/owid/api/v<version>/creator` and
|
|
26
|
+
`/owid/api/v<version>/verify`, and the version in the path is the version
|
|
27
|
+
byte of the OWID being verified rather than a fixed number, because a
|
|
28
|
+
creator serves each version of the format at its own path and returns 404
|
|
29
|
+
for the others.
|
|
30
|
+
|
|
25
31
|
The public key is requested for the OWID's own creation date
|
|
26
32
|
(`?date=<minutes>`), so OWIDs signed before a signing-key rotation still
|
|
27
33
|
verify. A creator that does not support the `date` parameter ignores it and
|
|
@@ -38,6 +44,12 @@ The headers are sent to every creator domain that a verification touches,
|
|
|
38
44
|
so only set a credential that all the creators in the tree are meant to
|
|
39
45
|
see.
|
|
40
46
|
|
|
47
|
+
A custom header makes the key request a non simple cross origin request, so
|
|
48
|
+
the browser sends a `OPTIONS` preflight first and the creator has to answer
|
|
49
|
+
it with `Access-Control-Allow-Headers` naming that header. A creator that
|
|
50
|
+
does not answer the preflight cannot be given a credential this way, and
|
|
51
|
+
the request never leaves the browser.
|
|
52
|
+
|
|
41
53
|
## Reading answers instead of throwing
|
|
42
54
|
|
|
43
55
|
An OWID is read from whatever a caller was handed, which on a public end
|
|
@@ -45,6 +45,25 @@ const otherKeyPair = nodeCrypto.generateKeyPairSync('ec', {
|
|
|
45
45
|
// 2021-04-06 12:59 UTC expressed as minutes since 2020-01-01 00:00 UTC.
|
|
46
46
|
const testDateInMinutes = 664619;
|
|
47
47
|
|
|
48
|
+
// The version byte every OWID built here is written with. The mocked
|
|
49
|
+
// creator only answers the path for this version, and the expected URL is
|
|
50
|
+
// built from the version the library read back out of the OWID, so the test
|
|
51
|
+
// and the library take the version from the same place and a wrong version
|
|
52
|
+
// in either one fails rather than passing quietly.
|
|
53
|
+
const testVersion = 3;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The creator key URL the library must request for an OWID. The version
|
|
57
|
+
* segment comes from the OWID's own version byte, which is the value the
|
|
58
|
+
* library uses, so this expectation cannot drift away from the code.
|
|
59
|
+
* @param {Object} o - the OWID that was read.
|
|
60
|
+
* @returns {string} the expected URL.
|
|
61
|
+
*/
|
|
62
|
+
function expectedCreatorUrl(o) {
|
|
63
|
+
return "//" + o.domain + "/owid/api/v" + o.version +
|
|
64
|
+
"/creator?date=" + o.date;
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
/**
|
|
49
68
|
* Reads an OWID that the test expects to be valid, asserting the three facts
|
|
50
69
|
* a read always reports before handing back the OWID itself.
|
|
@@ -64,8 +83,9 @@ const wrongKeyDomain = "wrong-key.swan-demo.uk";
|
|
|
64
83
|
const emptyKeyDomain = "empty-key.swan-demo.uk";
|
|
65
84
|
|
|
66
85
|
/**
|
|
67
|
-
* Builds the unsigned portion of
|
|
68
|
-
* same form the library serializes for
|
|
86
|
+
* Builds the unsigned portion of an OWID, at the version named by
|
|
87
|
+
* testVersion, as a byte array in the same form the library serializes for
|
|
88
|
+
* verification.
|
|
69
89
|
* @param {string} domain - the creator domain.
|
|
70
90
|
* @param {number} dateInMinutes - minutes since the OWID base date.
|
|
71
91
|
* @param {Buffer} payload - the payload bytes.
|
|
@@ -77,7 +97,7 @@ function buildUnsignedOWID(domain, dateInMinutes, payload) {
|
|
|
77
97
|
var length = Buffer.alloc(4);
|
|
78
98
|
length.writeUInt32LE(payload.length);
|
|
79
99
|
return Buffer.concat([
|
|
80
|
-
Buffer.from([
|
|
100
|
+
Buffer.from([testVersion]),
|
|
81
101
|
Buffer.from(domain, 'ascii'),
|
|
82
102
|
Buffer.from([0]),
|
|
83
103
|
date,
|
|
@@ -132,7 +152,10 @@ beforeEach(() => {
|
|
|
132
152
|
}
|
|
133
153
|
var url = new URL(urlString);
|
|
134
154
|
|
|
135
|
-
|
|
155
|
+
// A real creator serves each version of the format at its own
|
|
156
|
+
// path and returns 404 for the others, so answering any version
|
|
157
|
+
// here would hide a request sent to the wrong one.
|
|
158
|
+
if (url.pathname === "/owid/api/v" + testVersion + "/creator") {
|
|
136
159
|
// This domain returns a header only PEM with no key body, which
|
|
137
160
|
// exercises the empty public key guard in the library.
|
|
138
161
|
if (url.hostname == emptyKeyDomain) {
|
|
@@ -169,8 +192,65 @@ test('crypto verify valid OWID passes', () => {
|
|
|
169
192
|
// The library must have used the public key path, so the only
|
|
170
193
|
// request is to the creator end point.
|
|
171
194
|
expect(fetch.mock.calls.length).toBe(1);
|
|
195
|
+
expect(fetch.mock.calls[0][0]).toBe(expectedCreatorUrl(o));
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('crypto verify asks the end point for the version the OWID carries', () => {
|
|
200
|
+
// Every other OWID in this suite is version 3, the same value a constant
|
|
201
|
+
// in the path would give, so this is the test that catches a constant
|
|
202
|
+
// put back. A version 2 OWID must ask the version 2 end point, which
|
|
203
|
+
// the mocked creator does not serve, so the key is unavailable and the
|
|
204
|
+
// one request that was made names the right path.
|
|
205
|
+
var date = Buffer.alloc(4);
|
|
206
|
+
date.writeUInt32LE(testDateInMinutes);
|
|
207
|
+
var payload = Buffer.from("example");
|
|
208
|
+
var length = Buffer.alloc(4);
|
|
209
|
+
length.writeUInt32LE(payload.length);
|
|
210
|
+
var unsigned = Buffer.concat([
|
|
211
|
+
Buffer.from([2]),
|
|
212
|
+
Buffer.from(creatorDomain, 'ascii'),
|
|
213
|
+
Buffer.from([0]),
|
|
214
|
+
date,
|
|
215
|
+
length,
|
|
216
|
+
payload
|
|
217
|
+
]);
|
|
218
|
+
var o = read(signOWID(unsigned, creatorKeyPair.privateKey));
|
|
219
|
+
expect(o.version).toBe(2);
|
|
220
|
+
|
|
221
|
+
return o.checkSignature().then(r => {
|
|
222
|
+
expect(r.status).toBe(owid.SignatureStatus.KEY_UNAVAILABLE);
|
|
223
|
+
expect(fetch.mock.calls.length).toBe(1);
|
|
172
224
|
expect(fetch.mock.calls[0][0]).toBe(
|
|
173
|
-
"//" + creatorDomain + "/owid/api/
|
|
225
|
+
"//" + creatorDomain + "/owid/api/v2/creator?date=" +
|
|
226
|
+
testDateInMinutes);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// A creator whose domain answers with a redirect does not get the key at
|
|
231
|
+
// the other end trusted as its own. The library asks fetch not to follow,
|
|
232
|
+
// so the answer is the redirect itself, which is not ok and reads as the
|
|
233
|
+
// key being unavailable, and no second request is made. Without this a
|
|
234
|
+
// network attacker able to bend a creator's DNS, or a misconfigured
|
|
235
|
+
// creator, could substitute the key, and the publisher's fetchHeaders
|
|
236
|
+
// credential would travel to wherever the redirect pointed.
|
|
237
|
+
test('crypto verify does not follow a redirect from the creator', () => {
|
|
238
|
+
var redirectDomain = "redirecting.example";
|
|
239
|
+
fetchMock.mockResponse(req => {
|
|
240
|
+
return Promise.resolve({
|
|
241
|
+
status: 302,
|
|
242
|
+
headers: { Location: "https://elsewhere.example/key.pem" },
|
|
243
|
+
body: ""
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
var unsigned = buildUnsignedOWID(
|
|
247
|
+
redirectDomain, testDateInMinutes, Buffer.from("example"));
|
|
248
|
+
var o = read(signOWID(unsigned, creatorKeyPair.privateKey));
|
|
249
|
+
|
|
250
|
+
return o.checkSignature().then(r => {
|
|
251
|
+
expect(r.status).toBe(owid.SignatureStatus.KEY_UNAVAILABLE);
|
|
252
|
+
expect(fetch.mock.calls.length).toBe(1);
|
|
253
|
+
expect(fetch.mock.calls[0][1].redirect).toBe("manual");
|
|
174
254
|
});
|
|
175
255
|
});
|
|
176
256
|
|
|
@@ -139,6 +139,18 @@ fixtures.forEach(f => {
|
|
|
139
139
|
publicKeys[f.domain] = f.publicKeySPKI;
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* The creator key URL the library must request for an OWID. The version
|
|
144
|
+
* segment comes from the OWID's own version byte, which is the value the
|
|
145
|
+
* library uses, so this expectation cannot drift away from the code.
|
|
146
|
+
* @param {Object} o - the OWID that was read.
|
|
147
|
+
* @returns {string} the expected URL.
|
|
148
|
+
*/
|
|
149
|
+
function expectedCreatorUrl(o) {
|
|
150
|
+
return "//" + o.domain + "/owid/api/v" + o.version +
|
|
151
|
+
"/creator?date=" + o.date;
|
|
152
|
+
}
|
|
153
|
+
|
|
142
154
|
/**
|
|
143
155
|
* Returns the base 64 string with a single byte changed at the given offset
|
|
144
156
|
* of the decoded byte array.
|
|
@@ -164,7 +176,12 @@ beforeEach(() => {
|
|
|
164
176
|
}
|
|
165
177
|
var url = new URL(urlString);
|
|
166
178
|
|
|
167
|
-
|
|
179
|
+
// Every fixture is a version 3 OWID and a real creator serves each
|
|
180
|
+
// version of the format at its own path, returning 404 for the
|
|
181
|
+
// others, so the key is only handed back on the path that names the
|
|
182
|
+
// version the OWID was written in.
|
|
183
|
+
if (url.pathname === "/owid/api/v3/creator" &&
|
|
184
|
+
publicKeys[url.hostname]) {
|
|
168
185
|
return Promise.resolve(JSON.stringify({
|
|
169
186
|
publicKeySPKI: publicKeys[url.hostname]
|
|
170
187
|
}));
|
|
@@ -186,8 +203,8 @@ fixtures.forEach(f => {
|
|
|
186
203
|
// The library must have used the public key path, so the only
|
|
187
204
|
// request is to the creator end point.
|
|
188
205
|
expect(fetch.mock.calls.length).toBe(1);
|
|
189
|
-
expect(fetch.mock.calls[0][0]).toBe(
|
|
190
|
-
|
|
206
|
+
expect(fetch.mock.calls[0][0]).toBe(expectedCreatorUrl(o));
|
|
207
|
+
expect(o.version).toBe(3);
|
|
191
208
|
});
|
|
192
209
|
});
|
|
193
210
|
|