-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathtdf.spec.ts
More file actions
356 lines (308 loc) · 12 KB
/
Copy pathtdf.spec.ts
File metadata and controls
356 lines (308 loc) · 12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
import { expect } from 'chai';
import * as TDF from '../../../tdf3/src/tdf.js';
import { KeyAccessObject } from '../../../tdf3/src/models/key-access.js';
import { PolicyBody, type Policy } from '../../../tdf3/src/models/policy.js';
import { OriginAllowList } from '../../../src/access.js';
import { ConfigurationError, InvalidFileError, UnsafeUrlError } from '../../../src/errors.js';
import { getMocks } from '../../mocks/index.js';
import * as DefaultCryptoService from '../../../tdf3/src/crypto/index.js';
import type { CryptoService } from '../../../tdf3/src/crypto/declarations.js';
const sampleCert = `
-----BEGIN CERTIFICATE-----
MIIFnjCCA4YCCQCnKw0cfbMLJTANBgkqhkiG9w0BAQsFADCBkDELMAkGA1UEBhMC
VUExCzAJBgNVBAgMAk9EMQ4wDAYDVQQHDAVPREVTQTEPMA0GA1UECgwGVklSVFJV
MREwDwYDVQQLDAhQTEFURk9STTEZMBcGA1UEAwwQbG9jYWwudmlydHJ1LmNvbTEl
MCMGCSqGSIb3DQEJARYWc2l2YW5vdi5jdHJAdmlydHJ1LmNvbTAeFw0yMzA3MDYx
MTUxMzFaFw0yNDA3MDUxMTUxMzFaMIGQMQswCQYDVQQGEwJVQTELMAkGA1UECAwC
T0QxDjAMBgNVBAcMBU9ERVNBMQ8wDQYDVQQKDAZWSVJUUlUxETAPBgNVBAsMCFBM
QVRGT1JNMRkwFwYDVQQDDBBsb2NhbC52aXJ0cnUuY29tMSUwIwYJKoZIhvcNAQkB
FhZzaXZhbm92LmN0ckB2aXJ0cnUuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
MIICCgKCAgEAn+CvhpQZ6lPZG8j07l8JWWQnmPATKB2GDbcyC/Yg1rJAcFLZmBvH
VR3SamYQrSKxFRsoBGVruIFQBpoYMSolRWVVRnRBmpB5O8vCdP8J78mRaXGoJAez
xexuZBEoUIJJAn0Uoxg462201RgzLrZq+b+gOHZzcOfMlh/HPWhTeLdW2i0oMddL
yeykUa8ARHeCbA1Ux/IZju9n+lY3Pv7pI7z0mYuU14p33lvQbTZ1psY4frwFeyqC
rqHR1NqKjX+CnitFDrVKV48ZNlBQ6y348NpCaNH0wmy4FRAuGS6dJb9KFK6C+KIj
EQlWaucPTgvf6/dgZ6IZdLPvHUQNwC9yidRpnyLTUZlxwCXVO3P1sP7ifq75RwFk
OhvGqnoWNxUwd+23YUOFhE5b7WSO8NwRiMPBeEhNiWaiCmu1v88h52mgRr74MjCQ
mftuJlPwhsqWMpdYuv42851Y95x5s7BFWtntBNcU/+TTSKcygPNGkuR/tNw79JrM
bMl/Wj+cJoCW/3+1KzmPX13Nf2Cq2KZ8Kx6JCy+zFj/Hu8O/iqZ1vxA7gUyESPrd
NFB988QD7H8AQLCeDh6mOTdK08ZG74FlRBSk788A4oyslx8DslOkWbqHLdA7frv6
rEtl3mJ9UchFVpJOXw5nT3plE0ZRx4NWRLAG2+8GIUsL6eylpFUfVH8CAwEAATAN
BgkqhkiG9w0BAQsFAAOCAgEAlS3ph3ZcPf90uo0Y0l6Pca5I7ztuq6wu6reKAqnz
kh6N1rpLSe6EdRIOOCkMWMYTlZYFnSG/USGRfiCV0TOoo2C+jFvARRn6oir1v3B4
XBQORRNrOP8xV6kXAmNxs9Zl8CpAxfejV0vBHzN709q2HDfwmMmmp/yhlsAa1/iL
ARYV3TIJBmAOpec2CemuSVwGvYF7Ojl4sTYl0qMzrmm85wMBHJWR9EJbbFZBDDi4
GKVx26SfxaeauZXRgbO7Tav0q7mbJXI6u95aWO6iwkABRfnEQbimfVCjRI4TKlNs
WX5PS3QXXJg/9ocEglNGDwTouDwD/yMevR2tJ4clb9SJkJjJ3I0t7aUoFHXBl0B2
merKiN1ZE4OlbG/BsJypwAqV7Xk6tK3LM8fCMxhP1K0XX5Ifi1+hbN4Wr7a/zpEq
RWFMXtPKqTne0Ut6M7Xv5BBjY3uBmp0AX9hXt00olAiZsrOfj4gjAVULxdsbaEpr
nTqI65s26fls4y9bCZSfY//YNMAAtSRfmGbCZfrnzyvMtZfSMHqxhzMX7jXonV9c
TuHue7faEGaGlWGQVdNeudZaQ/eimDWeWnbhoUz6hSX7NICUFE64W6+GVFsAte3s
jtfbjUcLqvZzZt+u7YDapWbEbdgYjn/lUt0sTNyY65IagXJel9iacjCIVdzoKPzn
HJg=
-----END CERTIFICATE-----
`.trim();
const { kasECCert } = getMocks();
describe('TDF', () => {
const cryptoService: CryptoService = DefaultCryptoService;
it('should return key', async () => {
const pem = await TDF.extractPemFromKeyString(sampleCert, 'rsa:2048', cryptoService);
expect(pem).to.include('-----BEGIN PUBLIC KEY-----');
expect(pem).to.include('-----END PUBLIC KEY-----');
});
it('should return pem', async () => {
const sampleKey = sampleCert
.replace('BEGIN CERTIFICATE', 'BEGIN PUBLIC KEY')
.replace('END CERTIFICATE', 'END PUBLIC KEY');
const pem = await TDF.extractPemFromKeyString(sampleKey, 'rsa:2048', cryptoService);
expect(pem).to.equal(sampleKey);
});
it('should return ec pem', async () => {
const pem = await TDF.extractPemFromKeyString(kasECCert, 'ec:secp256r1', cryptoService);
expect(pem).to.include('-----BEGIN PUBLIC KEY-----');
expect(pem).to.include('-----END PUBLIC KEY-----');
});
});
describe('fetchKasPublicKey', async () => {
it('missing kas names throw', async () => {
try {
await TDF.fetchKasPublicKey('');
expect.fail('did not throw');
} catch (e) {
expect(() => {
throw e;
}).to.throw(ConfigurationError);
}
});
it('invalid kas names throw', async () => {
try {
await TDF.fetchKasPublicKey('~~~');
expect.fail('did not throw');
} catch (e) {
expect(e).to.exist;
}
});
it('localhost kas is valid', async () => {
const pk2 = await TDF.fetchKasPublicKey('http://localhost:3000');
expect(pk2.publicKey).to.include('BEGIN CERTIFICATE');
expect(pk2.kid).to.equal('e1');
});
it('invalid algorithms', async () => {
try {
const res = await TDF.fetchKasPublicKey('http://localhost:3000', 'rsa:512' as never); //ts-ignore
console.log(res);
expect.fail('did not throw');
} catch (e) {
expect(!!e).to.equal(true);
}
});
it('localhost BaseKey', async () => {
const pk2 = await TDF.fetchKasPublicKey('http://localhost:3000');
expect(pk2.publicKey).to.include('BEGIN CERTIFICATE');
expect(pk2.kid).to.equal('e1');
});
});
describe('validatePolicyObject', () => {
const testCases: { title: string; policy: Partial<Policy>; error?: string }[] = [
{
title: 'missing uuid',
policy: { body: { dataAttributes: [], dissem: ['someDissem'] } },
error: 'uuid',
},
{
title: 'missing body',
policy: { uuid: 'someUuid' },
error: 'body',
},
{
title: 'missing body.dissem',
policy: { uuid: 'someUuid', body: {} as PolicyBody },
error: 'dissem',
},
{
title: 'valid policy',
policy: { uuid: 'someUuid', body: { dataAttributes: [], dissem: ['someDissem'] } },
},
];
testCases.forEach(({ title, policy, error }) => {
it(`should handle ${title}`, () => {
if (error) {
expect(() => TDF.validatePolicyObject(policy as Policy)).to.throw(
ConfigurationError,
error
);
} else {
expect(() => TDF.validatePolicyObject(policy as Policy)).to.not.throw();
}
});
});
});
function createDeferred<T>() {
let reject!: (error: unknown) => void;
let resolve!: (value: T | PromiseLike<T>) => void;
const promise = new Promise<T>((innerResolve, innerReject) => {
resolve = innerResolve;
reject = innerReject;
});
return { promise, reject, resolve };
}
async function flushScheduler() {
await Promise.resolve();
await Promise.resolve();
}
describe('bounded segment scheduler', () => {
it('honors the configured batch size', async () => {
const started: Array<[number, number]> = [];
const deferred = createDeferred<void>();
const scheduler = TDF.createBoundedSegmentScheduler({
totalSegments: 6,
segmentBatchSize: 2,
maxConcurrentSegmentBatches: 1,
scheduleBatch: (startIndex, endIndex) => {
started.push([startIndex, endIndex]);
return deferred.promise;
},
});
scheduler.fillWindow();
await flushScheduler();
expect(started).to.deep.equal([[0, 2]]);
deferred.resolve();
await flushScheduler();
scheduler.markConsumed();
await flushScheduler();
expect(started).to.deep.equal([[0, 2]]);
scheduler.markConsumed();
await flushScheduler();
expect(started).to.deep.equal([
[0, 2],
[2, 4],
]);
});
it('bounds scheduled work to the configured prefetch window', async () => {
const started: Array<[number, number]> = [];
const deferredBatches = [
createDeferred<void>(),
createDeferred<void>(),
createDeferred<void>(),
];
let nextDeferred = 0;
const scheduler = TDF.createBoundedSegmentScheduler({
totalSegments: 10,
segmentBatchSize: 2,
maxConcurrentSegmentBatches: 2,
scheduleBatch: (startIndex, endIndex) => {
started.push([startIndex, endIndex]);
return deferredBatches[nextDeferred++].promise;
},
});
scheduler.fillWindow();
await flushScheduler();
expect(started).to.deep.equal([
[0, 2],
[2, 4],
]);
expect(scheduler.snapshot()).to.deep.equal({
consumedSegments: 0,
inFlightBatches: 2,
maxPrefetchedSegments: 4,
scheduledSegments: 4,
});
deferredBatches[0].resolve();
await flushScheduler();
expect(started).to.deep.equal([
[0, 2],
[2, 4],
]);
scheduler.markConsumed(2);
await flushScheduler();
expect(started).to.deep.equal([
[0, 2],
[2, 4],
[4, 6],
]);
});
});
describe('splitLookupTableFactory', () => {
it('should return a correct split table for valid input', () => {
const keyAccess: KeyAccessObject[] = [
{ sid: 'split1', type: 'remote', url: 'https://kas1', protocol: 'kas' },
{ sid: 'split2', type: 'remote', url: 'https://kas2', protocol: 'kas' },
];
const allowedKases = new OriginAllowList(['https://kas1', 'https://kas2']);
const result = TDF.splitLookupTableFactory(keyAccess, allowedKases);
expect(result).to.deep.equal({
split1: { 'https://kas1': [keyAccess[0]] },
split2: { 'https://kas2': [keyAccess[1]] },
});
});
it('should return a correct split table for valid input with ignoreAllowList', () => {
const keyAccess: KeyAccessObject[] = [
{ sid: 'split1', type: 'remote', url: 'https://kas1', protocol: 'kas' },
{ sid: 'split2', type: 'remote', url: 'https://kas2', protocol: 'kas' },
];
const allowedKases = new OriginAllowList([], true);
const result = TDF.splitLookupTableFactory(keyAccess, allowedKases);
expect(result).to.deep.equal({
split1: { 'https://kas1': [keyAccess[0]] },
split2: { 'https://kas2': [keyAccess[1]] },
});
});
it('should throw UnsafeUrlError for disallowed KASes', () => {
const keyAccess: KeyAccessObject[] = [
{ sid: 'split1', type: 'remote', url: 'https://kas1', protocol: 'kas' },
{ sid: 'split2', type: 'remote', url: 'https://kas3', protocol: 'kas' }, // kas3 is not allowed
];
const allowedKases = new OriginAllowList(['https://kas1']);
expect(() => TDF.splitLookupTableFactory(keyAccess, allowedKases)).to.throw(
UnsafeUrlError,
'Unreconstructable key - disallowed KASes include: ["https://kas3"] from splitIds ["split1","split2"]'
);
});
it('preserves duplicate URLs in the same splitId as a list', () => {
const keyAccess: KeyAccessObject[] = [
{ sid: 'split1', type: 'remote', url: 'https://kas1', protocol: 'kas' },
{ sid: 'split1', type: 'remote', url: 'https://kas1', protocol: 'kas' },
];
const allowedKases = new OriginAllowList(['https://kas1']);
const result = TDF.splitLookupTableFactory(keyAccess, allowedKases);
expect(result).to.deep.equal({
split1: { 'https://kas1': [keyAccess[0], keyAccess[1]] },
});
});
it('preserves multiple keys with distinct kids on the same KAS and split', () => {
const keyAccess: KeyAccessObject[] = [
{ sid: 'split1', type: 'wrapped', url: 'https://kas1', kid: 'k1', protocol: 'kas' },
{ sid: 'split1', type: 'wrapped', url: 'https://kas1', kid: 'k2', protocol: 'kas' },
];
const allowedKases = new OriginAllowList(['https://kas1']);
const result = TDF.splitLookupTableFactory(keyAccess, allowedKases);
expect(result['split1']['https://kas1']).to.have.length(2);
expect(result['split1']['https://kas1'].map((k) => k.kid)).to.deep.equal(['k1', 'k2']);
});
it('should handle empty keyAccess array', () => {
const keyAccess: KeyAccessObject[] = [];
const allowedKases = new OriginAllowList([]);
const result = TDF.splitLookupTableFactory(keyAccess, allowedKases);
expect(result).to.deep.equal({});
});
it('should handle empty allowedKases array', () => {
const keyAccess: KeyAccessObject[] = [
{ sid: 'split1', type: 'remote', url: 'https://kas1', protocol: 'kas' },
];
const allowedKases = new OriginAllowList([]);
expect(() => TDF.splitLookupTableFactory(keyAccess, allowedKases)).to.throw(
InvalidFileError,
'Unreconstructable key - disallowed KASes include: ["https://kas1"]'
);
});
it('should handle cases where sid is undefined', () => {
const keyAccess: KeyAccessObject[] = [
{ sid: undefined, type: 'remote', url: 'https://kas1', protocol: 'kas' },
];
const allowedKases = ['https://kas1'];
const result = TDF.splitLookupTableFactory(keyAccess, new OriginAllowList(allowedKases));
expect(result).to.deep.equal({
'': { 'https://kas1': [keyAccess[0]] },
});
});
});