@ibgib/core-gib 0.1.63 → 0.1.64
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/dist/keystone/keystone-constants.d.mts +1 -0
- package/dist/keystone/keystone-constants.d.mts.map +1 -1
- package/dist/keystone/keystone-constants.mjs +1 -0
- package/dist/keystone/keystone-constants.mjs.map +1 -1
- package/dist/keystone/keystone-helpers.d.mts +19 -1
- package/dist/keystone/keystone-helpers.d.mts.map +1 -1
- package/dist/keystone/keystone-helpers.mjs +191 -16
- package/dist/keystone/keystone-helpers.mjs.map +1 -1
- package/dist/keystone/keystone-service-v1.d.mts +82 -1
- package/dist/keystone/keystone-service-v1.d.mts.map +1 -1
- package/dist/keystone/keystone-service-v1.mjs +321 -27
- package/dist/keystone/keystone-service-v1.mjs.map +1 -1
- package/dist/keystone/keystone-service-v1.respec.mjs +474 -47
- package/dist/keystone/keystone-service-v1.respec.mjs.map +1 -1
- package/dist/keystone/keystone-types.d.mts +26 -2
- package/dist/keystone/keystone-types.d.mts.map +1 -1
- package/dist/keystone/keystone-types.mjs +13 -0
- package/dist/keystone/keystone-types.mjs.map +1 -1
- package/dist/keystone/policy/profiles/profile-domain.json +0 -15
- package/dist/keystone/policy/schemas/keystone.high.schema.json +1 -1
- package/dist/keystone/policy/schemas/keystone.medium.schema.json +1 -1
- package/package.json +2 -2
- package/src/keystone/keystone-constants.mts +1 -0
- package/src/keystone/keystone-helpers.mts +223 -15
- package/src/keystone/keystone-service-v1.mts +387 -28
- package/src/keystone/keystone-service-v1.respec.mts +1990 -1500
- package/src/keystone/keystone-types.mts +43 -2
- package/src/keystone/policy/profiles/profile-domain.json +1 -16
- package/src/keystone/policy/schemas/keystone.high.schema.json +1 -1
- package/src/keystone/policy/schemas/keystone.medium.schema.json +1 -1
|
@@ -1,1559 +1,2049 @@
|
|
|
1
|
-
import {
|
|
2
|
-
respecfully, iReckon, ifWe, firstOfAll, firstOfEach, lastOfAll, lastOfEach, respecfullyDear, ifWeMight
|
|
3
|
-
} from '@ibgib/helper-gib/dist/respec-gib/respec-gib.mjs';
|
|
4
|
-
const maam = `[${import.meta.url}]`, sir = maam;
|
|
5
|
-
import { clone, hash, getUUID } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
|
|
6
|
-
import { IbGib_V1 } from '@ibgib/ts-gib/dist/V1/types.mjs';
|
|
7
|
-
import { getIbGibAddr } from '@ibgib/ts-gib/dist/helper.mjs';
|
|
8
|
-
|
|
9
|
-
import { GLOBAL_LOG_A_LOT } from '../core-constants.mjs';
|
|
10
|
-
import { KeystoneStrategyFactory } from './strategy/keystone-strategy-factory.mjs';
|
|
11
|
-
import { KeystoneChallengePool, KeystoneClaim, KeystoneIbGib_V1, KeystonePoolConfig_HashV1 } from './keystone-types.mjs';
|
|
12
|
-
import { createRevocationPoolConfig, createStandardPoolConfig } from './keystone-config-builder.mjs';
|
|
13
|
-
import { POOL_ID_DEFAULT, POOL_ID_REVOKE, KEYSTONE_VERB_REVOKE, KEYSTONE_VERB_MANAGE, POOL_ID_MANAGE, KEYSTONE_VERB_SYNC } from './keystone-constants.mjs';
|
|
14
|
-
import { KeystoneService_V1, } from './keystone-service-v1.mjs';
|
|
15
|
-
import { selectChallengeIds, validateChallengePool, validateKeystoneIb, validateKeystoneTransition, validateGenesisKeystone } from './keystone-helpers.mjs';
|
|
16
|
-
import { KeystoneProfileBuilder } from './policy/keystone-profile-builder.mjs';
|
|
17
|
-
import { getTjpAddr } from '../common/other/ibgib-helper.mjs';
|
|
18
|
-
import { deriveDelegateSecret } from './strategy/hash-reveal-v1/hash-reveal-v1.mjs';
|
|
19
|
-
|
|
20
|
-
const logalot = GLOBAL_LOG_A_LOT;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* A simple in-memory map acting as a Space.
|
|
25
|
-
* Pure Storage. No Indexing logic.
|
|
26
|
-
*/
|
|
27
|
-
class MockIbGibSpace {
|
|
28
|
-
store = new Map<string, IbGib_V1>();
|
|
29
|
-
|
|
30
|
-
constructor(public name: string = "mock_space") { }
|
|
31
|
-
|
|
32
|
-
async put({ ibGib }: { ibGib: IbGib_V1 }): Promise<void> {
|
|
33
|
-
const addr = getIbGibAddr({ ibGib });
|
|
34
|
-
this.store.set(addr, JSON.parse(JSON.stringify(ibGib))); // Deep copy
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async get({ addr }: { addr: string }): Promise<IbGib_V1 | null> {
|
|
38
|
-
const data = this.store.get(addr);
|
|
39
|
-
return data ? JSON.parse(JSON.stringify(data)) : null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async argy({ argData, ibGibs }: { argData: any; ibGibs?: IbGib_V1[] }): Promise<any> {
|
|
43
|
-
return {
|
|
44
|
-
ib: 'arg^gib',
|
|
45
|
-
gib: 'arg_gib',
|
|
46
|
-
data: argData,
|
|
47
|
-
ibGibs,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async witness(arg: any): Promise<any> {
|
|
52
|
-
const cmd = arg.data?.cmd;
|
|
53
|
-
const cmdModifiers = arg.data?.cmdModifiers || [];
|
|
54
|
-
if (cmd === 'get') {
|
|
55
|
-
const addrs = arg.data.ibGibAddrs || [];
|
|
56
|
-
if (cmdModifiers.includes('latest') && cmdModifiers.includes('addrs')) {
|
|
57
|
-
const latestAddrsMap: { [addr: string]: string } = {};
|
|
58
|
-
for (const queryAddr of addrs) {
|
|
59
|
-
const queryTjpGib = queryAddr.includes('.') ? queryAddr.split('.')[1] : queryAddr.split('^')[1];
|
|
60
|
-
let latestAddr = queryAddr;
|
|
61
|
-
let maxN = -1;
|
|
62
|
-
for (const [addr, ibGib] of this.store.entries()) {
|
|
63
|
-
const tjpGib = addr.includes('.') ? addr.split('.')[1] : addr.split('^')[1];
|
|
64
|
-
if (tjpGib === queryTjpGib) {
|
|
65
|
-
const n = ibGib.data?.n ?? 0;
|
|
66
|
-
if (n > maxN) {
|
|
67
|
-
maxN = n;
|
|
68
|
-
latestAddr = addr;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
latestAddrsMap[queryAddr] = latestAddr;
|
|
73
|
-
}
|
|
74
|
-
return {
|
|
75
|
-
data: {
|
|
76
|
-
success: true,
|
|
77
|
-
latestAddrsMap,
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
} else {
|
|
81
|
-
const ibGibs: IbGib_V1[] = [];
|
|
82
|
-
for (const addr of addrs) {
|
|
83
|
-
const x = await this.get({ addr });
|
|
84
|
-
if (x) { ibGibs.push(x); }
|
|
85
|
-
}
|
|
86
|
-
return {
|
|
87
|
-
data: { success: true },
|
|
88
|
-
ibGibs,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return undefined;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* A partial mock of Metaspace.
|
|
99
|
-
* Handles:
|
|
100
|
-
* 1. Retrieving the local space.
|
|
101
|
-
* 2. Delegating 'put' to the space.
|
|
102
|
-
* 3. 'registerNewIbGib': Tracking the HEAD of a timeline.
|
|
103
|
-
*/
|
|
104
|
-
class MockMetaspaceService {
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Map of TJP Gib (Timeline ID) -> Latest IbGib Addr (Head)
|
|
108
|
-
*/
|
|
109
|
-
timelineHeads = new Map<string, string>();
|
|
110
|
-
|
|
111
|
-
constructor(public space: MockIbGibSpace) { }
|
|
112
|
-
|
|
113
|
-
async getLocalUserSpace({ lock }: { lock: boolean }): Promise<MockIbGibSpace> {
|
|
114
|
-
return this.space;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Metaspace often acts as a facade for put, defaulting to local space.
|
|
119
|
-
*/
|
|
120
|
-
async put(args: any): Promise<void> {
|
|
121
|
-
const target = args.space || this.space;
|
|
122
|
-
return target.put(args);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async get(args: { addr: string, space?: any }): Promise<{ success: boolean, ibGibs?: IbGib_V1[] }> {
|
|
126
|
-
const target = args.space || this.space;
|
|
127
|
-
const x = await target.get({ addr: args.addr });
|
|
128
|
-
return {
|
|
129
|
-
success: !!x,
|
|
130
|
-
ibGibs: x ? [x] : [],
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Tracks the latest version of an ibGib timeline.
|
|
136
|
-
*/
|
|
137
|
-
async registerNewIbGib(args: { ibGib: IbGib_V1, space?: any }): Promise<void> {
|
|
138
|
-
const { ibGib } = args;
|
|
139
|
-
const targetSpace = args.space || this.space;
|
|
140
|
-
|
|
141
|
-
// 1. Ensure it is stored
|
|
142
|
-
await targetSpace.put({ ibGib });
|
|
143
|
-
|
|
144
|
-
// 2. Extract TJP (Timeline Identifier)
|
|
145
|
-
// Simplified logic mirroring getGibInfo
|
|
146
|
-
const gib = ibGib.gib || '';
|
|
147
|
-
let tjpGib = gib;
|
|
148
|
-
|
|
149
|
-
if (gib.includes('.')) {
|
|
150
|
-
// It's a frame in a timeline: "punctiliarHash.tjpHash"
|
|
151
|
-
// The TJP is the suffix.
|
|
152
|
-
const parts = gib.split('.');
|
|
153
|
-
tjpGib = parts.slice(1).join('.');
|
|
154
|
-
}
|
|
155
|
-
// Else: It's a Primitive or a TJP itself (Genesis).
|
|
156
|
-
// If Genesis (isTjp=true), the gib IS the tjpGib.
|
|
157
|
-
|
|
158
|
-
const addr = getIbGibAddr({ ibGib });
|
|
159
|
-
this.timelineHeads.set(tjpGib, addr);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// ===========================================================================
|
|
164
|
-
// SUITE A: STRATEGY VECTORS (The Math)
|
|
165
|
-
// ===========================================================================
|
|
166
|
-
|
|
167
|
-
await respecfully(sir, 'Suite A: Strategy Vectors (HashRevealV1)', async () => {
|
|
168
|
-
|
|
169
|
-
// Setup generic variables
|
|
170
|
-
const masterSecret = "TestSecret_12345";
|
|
171
|
-
const poolId = "TestPool";
|
|
172
|
-
let config: KeystonePoolConfig_HashV1;
|
|
173
|
-
|
|
174
|
-
firstOfAll(sir, async () => {
|
|
175
|
-
// Use our standard builder to get a valid config object
|
|
176
|
-
const salt = (await getUUID()).substring(0, 16);
|
|
177
|
-
config = createStandardPoolConfig({
|
|
178
|
-
id: poolId,
|
|
179
|
-
salt,
|
|
180
|
-
}) as KeystonePoolConfig_HashV1;
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
await respecfully(sir, 'Derivation Logic', async () => {
|
|
184
|
-
|
|
185
|
-
await
|
|
186
|
-
const strategy = KeystoneStrategyFactory.create({ config });
|
|
187
|
-
|
|
188
|
-
const secretA = await strategy.derivePoolSecret({ masterSecret });
|
|
189
|
-
const secretB = await strategy.derivePoolSecret({ masterSecret });
|
|
190
|
-
|
|
191
|
-
iReckon(sir, secretA).asTo('secret consistency').willEqual(secretB);
|
|
192
|
-
iReckon(sir, secretA).asTo('secret length').isGonnaBeTruthy();
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
await
|
|
196
|
-
const strategy = KeystoneStrategyFactory.create({ config });
|
|
197
|
-
|
|
198
|
-
const secretA = await strategy.derivePoolSecret({ masterSecret });
|
|
199
|
-
const secretB = await strategy.derivePoolSecret({ masterSecret: masterSecret + "_diff" });
|
|
200
|
-
|
|
201
|
-
iReckon(sir, secretA).asTo('secrets differ').not.willEqual(secretB);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
await
|
|
205
|
-
// Modify salt in a copy of config
|
|
206
|
-
const configB = { ...config, salt: "OtherPool" };
|
|
207
|
-
const strategyA = KeystoneStrategyFactory.create({ config });
|
|
208
|
-
const strategyB = KeystoneStrategyFactory.create({ config: configB });
|
|
209
|
-
|
|
210
|
-
const secretA = await strategyA.derivePoolSecret({ masterSecret });
|
|
211
|
-
const secretB = await strategyB.derivePoolSecret({ masterSecret });
|
|
212
|
-
|
|
213
|
-
iReckon(sir, secretA).asTo('salt affects secret').not.willEqual(secretB);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
await
|
|
217
|
-
const secretA = await deriveDelegateSecret({ masterSecret });
|
|
218
|
-
const secretB = await deriveDelegateSecret({ masterSecret });
|
|
219
|
-
|
|
220
|
-
iReckon(sir, secretA).asTo('consistency').willEqual(secretB);
|
|
221
|
-
iReckon(sir, secretA).asTo('truthy').isGonnaBeTruthy();
|
|
222
|
-
|
|
223
|
-
const secretC = await deriveDelegateSecret({ masterSecret: masterSecret + '_other' });
|
|
224
|
-
iReckon(sir, secretA).asTo('differ').not.willEqual(secretC);
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
await respecfully(sir, 'Challenge/Solution Logic', async () => {
|
|
229
|
-
|
|
230
|
-
await
|
|
231
|
-
const strategy = KeystoneStrategyFactory.create({ config });
|
|
232
|
-
const poolSecret = await strategy.derivePoolSecret({ masterSecret });
|
|
233
|
-
const challengeId = "a3ff7843552870fc28bef2b"; // arbitrary random challengeId
|
|
234
|
-
|
|
235
|
-
// 1. Generate Solution
|
|
236
|
-
const solution = await strategy.generateSolution({ poolSecret, poolId: config.id, challengeId });
|
|
237
|
-
iReckon(sir, solution.value).asTo('solution value exists').isGonnaBeTruthy();
|
|
238
|
-
iReckon(sir, solution.challengeId).asTo('id matches').willEqual(challengeId);
|
|
239
|
-
|
|
240
|
-
// 2. Generate Public Challenge from Solution
|
|
241
|
-
const challenge = await strategy.generateChallenge({ solution });
|
|
242
|
-
iReckon(sir, challenge.hash).asTo('challenge hash exists').isGonnaBeTruthy();
|
|
243
|
-
|
|
244
|
-
// 3. Validate
|
|
245
|
-
const isValid = await strategy.validateSolution({ solution, challenge });
|
|
246
|
-
iReckon(sir, isValid).asTo('valid pair should pass').isGonnaBeTrue();
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
await
|
|
250
|
-
const strategy = KeystoneStrategyFactory.create({ config });
|
|
251
|
-
const poolSecret = await strategy.derivePoolSecret({ masterSecret });
|
|
252
|
-
const challengeId = "8c994f3ed598f150e25513"; // arbitrary random challengeId
|
|
253
|
-
|
|
254
|
-
// Generate real pair
|
|
255
|
-
const solution = await strategy.generateSolution({ poolSecret, poolId: config.id, challengeId });
|
|
256
|
-
const challenge = await strategy.generateChallenge({ solution });
|
|
257
|
-
|
|
258
|
-
// Tamper with solution value
|
|
259
|
-
const badSolution = { ...solution, value: "hacked_value" };
|
|
260
|
-
|
|
261
|
-
const isValid = await strategy.validateSolution({ solution: badSolution, challenge });
|
|
262
|
-
iReckon(sir, isValid).asTo('tampered solution should fail').isGonnaBeFalse();
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
await
|
|
266
|
-
const strategy = KeystoneStrategyFactory.create({ config });
|
|
267
|
-
const poolSecret = await strategy.derivePoolSecret({ masterSecret });
|
|
268
|
-
|
|
269
|
-
// Generate pair A
|
|
270
|
-
const challengeId_A = "416c38cfd6ee63dbf8d4e5ef36"; // arbitrary random challengeId
|
|
271
|
-
const solutionA = await strategy.generateSolution({ poolSecret, poolId: config.id, challengeId: challengeId_A });
|
|
272
|
-
|
|
273
|
-
// Generate pair B
|
|
274
|
-
const challengeId_B = "c487ef6b7878fae798c3"; // arbitrary random challengeId
|
|
275
|
-
const solutionB = await strategy.generateSolution({ poolSecret, poolId: config.id, challengeId: challengeId_B });
|
|
276
|
-
const challengeB = await strategy.generateChallenge({ solution: solutionB });
|
|
277
|
-
|
|
278
|
-
// Check A against B
|
|
279
|
-
const isValid = await strategy.validateSolution({ solution: solutionA, challenge: challengeB });
|
|
280
|
-
iReckon(sir, isValid).asTo('mismatched pair should fail').isGonnaBeFalse();
|
|
281
|
-
});
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
// ===========================================================================
|
|
286
|
-
// SUITE B: SERVICE LIFECYCLE (Genesis -> Sign -> Validate)
|
|
287
|
-
// ===========================================================================
|
|
288
|
-
|
|
289
|
-
await respecfully(sir, 'Suite B: Service Lifecycle', async () => {
|
|
290
|
-
|
|
291
|
-
const service = new KeystoneService_V1();
|
|
292
|
-
const masterSecret = "AliceSecretKey_987654321";
|
|
293
|
-
|
|
294
|
-
let mockSpace: MockIbGibSpace;
|
|
295
|
-
let mockMetaspace: any;
|
|
296
|
-
|
|
297
|
-
let genesisKeystone: KeystoneIbGib_V1;
|
|
298
|
-
let signedKeystone: KeystoneIbGib_V1;
|
|
299
|
-
|
|
300
|
-
firstOfAll(sir, async () => {
|
|
301
|
-
mockSpace = new MockIbGibSpace();
|
|
302
|
-
mockMetaspace = new MockMetaspaceService(mockSpace);
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
await respecfully(sir, 'Genesis', async () => {
|
|
306
|
-
await
|
|
307
|
-
const salt = (await getUUID()).substring(0, 16);
|
|
308
|
-
const config = createStandardPoolConfig({
|
|
309
|
-
id: POOL_ID_DEFAULT,
|
|
310
|
-
salt,
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
genesisKeystone = await service.genesis({
|
|
314
|
-
masterSecret,
|
|
315
|
-
configs: [config],
|
|
316
|
-
metaspace: mockMetaspace,
|
|
317
|
-
space: mockSpace as any,
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
// Verify Object
|
|
321
|
-
iReckon(sir, genesisKeystone).asTo('genesis object').isGonnaBeTruthy();
|
|
322
|
-
iReckon(sir, genesisKeystone.data?.isTjp).asTo('isTjp').isGonnaBeTrue();
|
|
323
|
-
|
|
324
|
-
// Verify Persistence
|
|
325
|
-
const addr = getIbGibAddr({ ibGib: genesisKeystone });
|
|
326
|
-
const saved = await mockSpace.get({ addr });
|
|
327
|
-
|
|
328
|
-
iReckon(sir, saved).asTo('persisted to space').isGonnaBeTruthy();
|
|
329
|
-
|
|
330
|
-
// Verify Registration (Timeline Tracking)
|
|
331
|
-
// Genesis gib should be registered as a timeline head
|
|
332
|
-
const head = mockMetaspace.timelineHeads.get(genesisKeystone.gib!);
|
|
333
|
-
iReckon(sir, head).asTo('genesis registered as timeline head').willEqual(addr);
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
await respecfully(sir, 'Signing (Evolution)', async () => {
|
|
338
|
-
await
|
|
339
|
-
const claim: Partial<KeystoneClaim> = {
|
|
340
|
-
target: "comment 123^gib",
|
|
341
|
-
verb: "post"
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
const details = { note: "First post!" };
|
|
345
|
-
|
|
346
|
-
signedKeystone = await service.sign({
|
|
347
|
-
latestKeystone: genesisKeystone,
|
|
348
|
-
masterSecret,
|
|
349
|
-
claim,
|
|
350
|
-
poolId: POOL_ID_DEFAULT,
|
|
351
|
-
frameDetails: details,
|
|
352
|
-
metaspace: mockMetaspace,
|
|
353
|
-
space: mockSpace as any,
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
iReckon(sir, signedKeystone).asTo('new frame created').isGonnaBeTruthy();
|
|
357
|
-
iReckon(sir, signedKeystone).asTo('is different frame').not.isGonnaBe(genesisKeystone);
|
|
358
|
-
|
|
359
|
-
// NOTE: If this fails, check if 'sign' calls 'space.put' or 'metaspace.put'!
|
|
360
|
-
// In your current 'sign' implementation, you return the object but might have missed the save step.
|
|
361
|
-
const addr = getIbGibAddr({ ibGib: signedKeystone });
|
|
362
|
-
const saved = await mockSpace.get({ addr });
|
|
363
|
-
iReckon(sir, saved).asTo('persisted to space').isGonnaBeTruthy();
|
|
364
|
-
});
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
await respecfully(sir, 'Validation', async () => {
|
|
368
|
-
await
|
|
369
|
-
const errors = await service.validate({
|
|
370
|
-
prevIbGib: genesisKeystone,
|
|
371
|
-
currentIbGib: signedKeystone,
|
|
372
|
-
// metaspace: mockMetaspace,
|
|
373
|
-
// space: mockSpace as any,
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
iReckon(sir, errors.length).asTo('signature validation has no errors').willEqual(0);
|
|
377
|
-
});
|
|
378
|
-
});
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
// ===========================================================================
|
|
382
|
-
// SUITE C: SECURITY & SAD PATHS
|
|
383
|
-
// ===========================================================================
|
|
384
|
-
|
|
385
|
-
await respecfully(sir, 'Suite C: Security Vectors', async () => {
|
|
386
|
-
|
|
387
|
-
const service = new KeystoneService_V1();
|
|
388
|
-
const aliceSecret = "AliceSecret_111";
|
|
389
|
-
const eveSecret = "EveSecret_666";
|
|
390
|
-
|
|
391
|
-
let mockSpace: MockIbGibSpace;
|
|
392
|
-
let mockMetaspace: any;
|
|
393
|
-
let genesisKeystone: KeystoneIbGib_V1;
|
|
394
|
-
|
|
395
|
-
firstOfAll(sir, async () => {
|
|
396
|
-
mockSpace = new MockIbGibSpace();
|
|
397
|
-
mockMetaspace = new MockMetaspaceService(mockSpace);
|
|
398
|
-
|
|
399
|
-
// Setup Alice's Identity
|
|
400
|
-
const salt = (await getUUID()).substring(0, 16);
|
|
401
|
-
const config = createStandardPoolConfig({
|
|
402
|
-
id: POOL_ID_DEFAULT,
|
|
403
|
-
salt,
|
|
404
|
-
targetBinding: 0,
|
|
405
|
-
size: 10,
|
|
406
|
-
});
|
|
407
|
-
// config.behavior.size = 10;
|
|
408
|
-
genesisKeystone = await service.genesis({
|
|
409
|
-
masterSecret: aliceSecret,
|
|
410
|
-
configs: [config],
|
|
411
|
-
metaspace: mockMetaspace,
|
|
412
|
-
space: mockSpace as any,
|
|
413
|
-
});
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
await respecfully(sir, 'Wrong Secret (Forgery)', async () => {
|
|
417
|
-
await
|
|
418
|
-
const claim: Partial<KeystoneClaim> = { target: "comment 123^gib", verb: "post" };
|
|
419
|
-
|
|
420
|
-
let errorCaught = false;
|
|
421
|
-
let errorMsg = "";
|
|
422
|
-
|
|
423
|
-
try {
|
|
424
|
-
// Eve tries to sign Alice's keystone.
|
|
425
|
-
// This MUST fail because sign() calls evolve(), which calls validate().
|
|
426
|
-
await service.sign({
|
|
427
|
-
latestKeystone: genesisKeystone,
|
|
428
|
-
masterSecret: eveSecret,
|
|
429
|
-
claim,
|
|
430
|
-
poolId: POOL_ID_DEFAULT,
|
|
431
|
-
metaspace: mockMetaspace,
|
|
432
|
-
space: mockSpace as any,
|
|
433
|
-
});
|
|
434
|
-
} catch (e: any) {
|
|
435
|
-
errorCaught = true;
|
|
436
|
-
errorMsg = e.message;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
iReckon(sir, errorCaught).asTo('service rejected forgery').isGonnaBeTrue();
|
|
440
|
-
// Verify it was a crypto error, not something else
|
|
441
|
-
iReckon(sir, errorMsg).asTo('error mentions crypto violation').includes('Crypto Violation');
|
|
442
|
-
});
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
await respecfully(sir, 'Policy Violation (Restricted Verbs)', async () => {
|
|
446
|
-
await
|
|
447
|
-
// Create a specific restricted pool config manually
|
|
448
|
-
const restrictedPoolId = "read_only_pool";
|
|
449
|
-
const restrictedConfig = createStandardPoolConfig({
|
|
450
|
-
id: restrictedPoolId,
|
|
451
|
-
salt: restrictedPoolId,
|
|
452
|
-
});
|
|
453
|
-
// Manually restrict it (since Builder defaults to undefined/allow-all)
|
|
454
|
-
restrictedConfig.allowedVerbs = ['read'];
|
|
455
|
-
|
|
456
|
-
const restrictedGenesis = await service.genesis({
|
|
457
|
-
masterSecret: aliceSecret,
|
|
458
|
-
configs: [restrictedConfig],
|
|
459
|
-
metaspace: mockMetaspace,
|
|
460
|
-
space: mockSpace as any,
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
// Try to sign "write" using "read_only_pool"
|
|
464
|
-
const claim: Partial<KeystoneClaim> = { target: "data^gib", verb: "write" };
|
|
465
|
-
|
|
466
|
-
let errorCaught = false;
|
|
467
|
-
try {
|
|
468
|
-
await service.sign({
|
|
469
|
-
latestKeystone: restrictedGenesis,
|
|
470
|
-
masterSecret: aliceSecret,
|
|
471
|
-
claim,
|
|
472
|
-
poolId: restrictedPoolId, // Force use of restricted pool
|
|
473
|
-
metaspace: mockMetaspace,
|
|
474
|
-
space: mockSpace as any,
|
|
475
|
-
});
|
|
476
|
-
} catch (e) {
|
|
477
|
-
errorCaught = true;
|
|
478
|
-
// Optional: Check error message contains "not authorized"
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
iReckon(sir, errorCaught).asTo('policy enforced').isGonnaBeTrue();
|
|
482
|
-
});
|
|
483
|
-
});
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
// ===========================================================================
|
|
487
|
-
// SUITE D: REVOCATION
|
|
488
|
-
// ===========================================================================
|
|
489
|
-
|
|
490
|
-
await respecfully(sir, 'Suite D: Revocation', async () => {
|
|
491
|
-
|
|
492
|
-
const service = new KeystoneService_V1();
|
|
493
|
-
const masterSecret = "AliceSecret_RevokeTest";
|
|
494
|
-
|
|
495
|
-
let mockSpace: MockIbGibSpace;
|
|
496
|
-
let mockMetaspace: any;
|
|
497
|
-
let genesisKeystone: KeystoneIbGib_V1;
|
|
498
|
-
|
|
499
|
-
firstOfAll(sir, async () => {
|
|
500
|
-
mockSpace = new MockIbGibSpace();
|
|
501
|
-
mockMetaspace = new MockMetaspaceService(mockSpace);
|
|
502
|
-
|
|
503
|
-
// Setup Identity WITH a Revocation Pool
|
|
504
|
-
const stdSalt = (await getUUID()).substring(0, 16);
|
|
505
|
-
const revokeSalt = (await getUUID()).substring(0, 16);
|
|
506
|
-
const stdConfig = createStandardPoolConfig({
|
|
507
|
-
id: POOL_ID_DEFAULT,
|
|
508
|
-
salt: stdSalt,
|
|
509
|
-
});
|
|
510
|
-
const revokeConfig = createRevocationPoolConfig({
|
|
511
|
-
id: POOL_ID_REVOKE,
|
|
512
|
-
salt: revokeSalt,
|
|
513
|
-
}); // Special Config
|
|
514
|
-
|
|
515
|
-
genesisKeystone = await service.genesis({
|
|
516
|
-
masterSecret,
|
|
517
|
-
configs: [stdConfig, revokeConfig],
|
|
518
|
-
metaspace: mockMetaspace,
|
|
519
|
-
space: mockSpace as any,
|
|
520
|
-
});
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
await respecfully(sir, 'Revoke Lifecycle', async () => {
|
|
524
|
-
let revokedKeystone: KeystoneIbGib_V1;
|
|
525
|
-
|
|
526
|
-
await
|
|
527
|
-
revokedKeystone = await service.revoke({
|
|
528
|
-
latestKeystone: genesisKeystone,
|
|
529
|
-
masterSecret,
|
|
530
|
-
reason: "Key compromised",
|
|
531
|
-
metaspace: mockMetaspace,
|
|
532
|
-
space: mockSpace as any,
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
iReckon(sir, revokedKeystone).isGonnaBeTruthy();
|
|
536
|
-
|
|
537
|
-
// Check Data
|
|
538
|
-
const data = revokedKeystone.data!;
|
|
539
|
-
iReckon(sir, data.revocationInfo).asTo('revocation info present').isGonnaBeTruthy();
|
|
540
|
-
iReckon(sir, data.revocationInfo!.reason).willEqual("Key compromised");
|
|
541
|
-
iReckon(sir, data.revocationInfo!.proof.claim.verb).willEqual(KEYSTONE_VERB_REVOKE);
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
await
|
|
545
|
-
const errors = await service.validate({
|
|
546
|
-
prevIbGib: genesisKeystone,
|
|
547
|
-
currentIbGib: revokedKeystone!,
|
|
548
|
-
// metaspace: mockMetaspace,
|
|
549
|
-
// space: mockSpace as any,
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
iReckon(sir, errors.length).asTo('no validation errors').willEqual(0);
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
await
|
|
556
|
-
const data = revokedKeystone!.data!;
|
|
557
|
-
const revokePool = data.challengePools.find(p => p.id === POOL_ID_REVOKE);
|
|
558
|
-
|
|
559
|
-
// The pool should exist...
|
|
560
|
-
iReckon(sir, revokePool).isGonnaBeTruthy();
|
|
561
|
-
|
|
562
|
-
// Should be empty (0 challenges)
|
|
563
|
-
const remaining = Object.keys(revokePool!.challenges);
|
|
564
|
-
iReckon(sir, remaining.length).asTo('pool depleted').willEqual(0);
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
1
|
+
import {
|
|
2
|
+
respecfully, iReckon, ifWe, firstOfAll, firstOfEach, lastOfAll, lastOfEach, respecfullyDear, ifWeMight
|
|
3
|
+
} from '@ibgib/helper-gib/dist/respec-gib/respec-gib.mjs';
|
|
4
|
+
const maam = `[${import.meta.url}]`, sir = maam;
|
|
5
|
+
import { clone, hash, getUUID } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
|
|
6
|
+
import { IbGib_V1 } from '@ibgib/ts-gib/dist/V1/types.mjs';
|
|
7
|
+
import { getIbGibAddr } from '@ibgib/ts-gib/dist/helper.mjs';
|
|
8
|
+
|
|
9
|
+
import { GLOBAL_LOG_A_LOT } from '../core-constants.mjs';
|
|
10
|
+
import { KeystoneStrategyFactory } from './strategy/keystone-strategy-factory.mjs';
|
|
11
|
+
import { KeystoneChallengePool, KeystoneClaim, KeystoneIbGib_V1, KeystonePoolConfig_HashV1, KeystoneReplenishStrategy, KeystoneClaimType, ClaimDetails_ChangePassword, ClaimDetails_AddPool, ClaimDetails_ReplacePool } from './keystone-types.mjs';
|
|
12
|
+
import { createRevocationPoolConfig, createStandardPoolConfig } from './keystone-config-builder.mjs';
|
|
13
|
+
import { POOL_ID_DEFAULT, POOL_ID_REVOKE, KEYSTONE_VERB_REVOKE, KEYSTONE_VERB_MANAGE, POOL_ID_MANAGE, KEYSTONE_VERB_SYNC, POOL_ID_CUSTODIAN_MANAGE } from './keystone-constants.mjs';
|
|
14
|
+
import { KeystoneService_V1, } from './keystone-service-v1.mjs';
|
|
15
|
+
import { selectChallengeIds, validateChallengePool, validateKeystoneIb, validateKeystoneTransition, validateGenesisKeystone, rotatePoolChallenges } from './keystone-helpers.mjs';
|
|
16
|
+
import { KeystoneProfileBuilder } from './policy/keystone-profile-builder.mjs';
|
|
17
|
+
import { getTjpAddr } from '../common/other/ibgib-helper.mjs';
|
|
18
|
+
import { deriveDelegateSecret } from './strategy/hash-reveal-v1/hash-reveal-v1.mjs';
|
|
19
|
+
|
|
20
|
+
const logalot = GLOBAL_LOG_A_LOT;
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A simple in-memory map acting as a Space.
|
|
25
|
+
* Pure Storage. No Indexing logic.
|
|
26
|
+
*/
|
|
27
|
+
class MockIbGibSpace {
|
|
28
|
+
store = new Map<string, IbGib_V1>();
|
|
29
|
+
|
|
30
|
+
constructor(public name: string = "mock_space") { }
|
|
31
|
+
|
|
32
|
+
async put({ ibGib }: { ibGib: IbGib_V1 }): Promise<void> {
|
|
33
|
+
const addr = getIbGibAddr({ ibGib });
|
|
34
|
+
this.store.set(addr, JSON.parse(JSON.stringify(ibGib))); // Deep copy
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async get({ addr }: { addr: string }): Promise<IbGib_V1 | null> {
|
|
38
|
+
const data = this.store.get(addr);
|
|
39
|
+
return data ? JSON.parse(JSON.stringify(data)) : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async argy({ argData, ibGibs }: { argData: any; ibGibs?: IbGib_V1[] }): Promise<any> {
|
|
43
|
+
return {
|
|
44
|
+
ib: 'arg^gib',
|
|
45
|
+
gib: 'arg_gib',
|
|
46
|
+
data: argData,
|
|
47
|
+
ibGibs,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async witness(arg: any): Promise<any> {
|
|
52
|
+
const cmd = arg.data?.cmd;
|
|
53
|
+
const cmdModifiers = arg.data?.cmdModifiers || [];
|
|
54
|
+
if (cmd === 'get') {
|
|
55
|
+
const addrs = arg.data.ibGibAddrs || [];
|
|
56
|
+
if (cmdModifiers.includes('latest') && cmdModifiers.includes('addrs')) {
|
|
57
|
+
const latestAddrsMap: { [addr: string]: string } = {};
|
|
58
|
+
for (const queryAddr of addrs) {
|
|
59
|
+
const queryTjpGib = queryAddr.includes('.') ? queryAddr.split('.')[1] : queryAddr.split('^')[1];
|
|
60
|
+
let latestAddr = queryAddr;
|
|
61
|
+
let maxN = -1;
|
|
62
|
+
for (const [addr, ibGib] of this.store.entries()) {
|
|
63
|
+
const tjpGib = addr.includes('.') ? addr.split('.')[1] : addr.split('^')[1];
|
|
64
|
+
if (tjpGib === queryTjpGib) {
|
|
65
|
+
const n = ibGib.data?.n ?? 0;
|
|
66
|
+
if (n > maxN) {
|
|
67
|
+
maxN = n;
|
|
68
|
+
latestAddr = addr;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
latestAddrsMap[queryAddr] = latestAddr;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
data: {
|
|
76
|
+
success: true,
|
|
77
|
+
latestAddrsMap,
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
} else {
|
|
81
|
+
const ibGibs: IbGib_V1[] = [];
|
|
82
|
+
for (const addr of addrs) {
|
|
83
|
+
const x = await this.get({ addr });
|
|
84
|
+
if (x) { ibGibs.push(x); }
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
data: { success: true },
|
|
88
|
+
ibGibs,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A partial mock of Metaspace.
|
|
99
|
+
* Handles:
|
|
100
|
+
* 1. Retrieving the local space.
|
|
101
|
+
* 2. Delegating 'put' to the space.
|
|
102
|
+
* 3. 'registerNewIbGib': Tracking the HEAD of a timeline.
|
|
103
|
+
*/
|
|
104
|
+
class MockMetaspaceService {
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Map of TJP Gib (Timeline ID) -> Latest IbGib Addr (Head)
|
|
108
|
+
*/
|
|
109
|
+
timelineHeads = new Map<string, string>();
|
|
110
|
+
|
|
111
|
+
constructor(public space: MockIbGibSpace) { }
|
|
112
|
+
|
|
113
|
+
async getLocalUserSpace({ lock }: { lock: boolean }): Promise<MockIbGibSpace> {
|
|
114
|
+
return this.space;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Metaspace often acts as a facade for put, defaulting to local space.
|
|
119
|
+
*/
|
|
120
|
+
async put(args: any): Promise<void> {
|
|
121
|
+
const target = args.space || this.space;
|
|
122
|
+
return target.put(args);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async get(args: { addr: string, space?: any }): Promise<{ success: boolean, ibGibs?: IbGib_V1[] }> {
|
|
126
|
+
const target = args.space || this.space;
|
|
127
|
+
const x = await target.get({ addr: args.addr });
|
|
128
|
+
return {
|
|
129
|
+
success: !!x,
|
|
130
|
+
ibGibs: x ? [x] : [],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Tracks the latest version of an ibGib timeline.
|
|
136
|
+
*/
|
|
137
|
+
async registerNewIbGib(args: { ibGib: IbGib_V1, space?: any }): Promise<void> {
|
|
138
|
+
const { ibGib } = args;
|
|
139
|
+
const targetSpace = args.space || this.space;
|
|
140
|
+
|
|
141
|
+
// 1. Ensure it is stored
|
|
142
|
+
await targetSpace.put({ ibGib });
|
|
143
|
+
|
|
144
|
+
// 2. Extract TJP (Timeline Identifier)
|
|
145
|
+
// Simplified logic mirroring getGibInfo
|
|
146
|
+
const gib = ibGib.gib || '';
|
|
147
|
+
let tjpGib = gib;
|
|
148
|
+
|
|
149
|
+
if (gib.includes('.')) {
|
|
150
|
+
// It's a frame in a timeline: "punctiliarHash.tjpHash"
|
|
151
|
+
// The TJP is the suffix.
|
|
152
|
+
const parts = gib.split('.');
|
|
153
|
+
tjpGib = parts.slice(1).join('.');
|
|
154
|
+
}
|
|
155
|
+
// Else: It's a Primitive or a TJP itself (Genesis).
|
|
156
|
+
// If Genesis (isTjp=true), the gib IS the tjpGib.
|
|
157
|
+
|
|
158
|
+
const addr = getIbGibAddr({ ibGib });
|
|
159
|
+
this.timelineHeads.set(tjpGib, addr);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ===========================================================================
|
|
164
|
+
// SUITE A: STRATEGY VECTORS (The Math)
|
|
165
|
+
// ===========================================================================
|
|
166
|
+
|
|
167
|
+
await respecfully(sir, 'Suite A: Strategy Vectors (HashRevealV1)', async () => {
|
|
168
|
+
|
|
169
|
+
// Setup generic variables
|
|
170
|
+
const masterSecret = "TestSecret_12345";
|
|
171
|
+
const poolId = "TestPool";
|
|
172
|
+
let config: KeystonePoolConfig_HashV1;
|
|
173
|
+
|
|
174
|
+
firstOfAll(sir, async () => {
|
|
175
|
+
// Use our standard builder to get a valid config object
|
|
176
|
+
const salt = (await getUUID()).substring(0, 16);
|
|
177
|
+
config = createStandardPoolConfig({
|
|
178
|
+
id: poolId,
|
|
179
|
+
salt,
|
|
180
|
+
}) as KeystonePoolConfig_HashV1;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await respecfully(sir, 'Derivation Logic', async () => {
|
|
184
|
+
|
|
185
|
+
await ifWeMight(sir, 'derivePoolSecret with same inputs returns same output', async () => {
|
|
186
|
+
const strategy = KeystoneStrategyFactory.create({ config });
|
|
187
|
+
|
|
188
|
+
const secretA = await strategy.derivePoolSecret({ masterSecret });
|
|
189
|
+
const secretB = await strategy.derivePoolSecret({ masterSecret });
|
|
190
|
+
|
|
191
|
+
iReckon(sir, secretA).asTo('secret consistency').willEqual(secretB);
|
|
192
|
+
iReckon(sir, secretA).asTo('secret length').isGonnaBeTruthy();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await ifWeMight(sir, 'derivePoolSecret with different master secret returns different output', async () => {
|
|
196
|
+
const strategy = KeystoneStrategyFactory.create({ config });
|
|
197
|
+
|
|
198
|
+
const secretA = await strategy.derivePoolSecret({ masterSecret });
|
|
199
|
+
const secretB = await strategy.derivePoolSecret({ masterSecret: masterSecret + "_diff" });
|
|
200
|
+
|
|
201
|
+
iReckon(sir, secretA).asTo('secrets differ').not.willEqual(secretB);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await ifWeMight(sir, 'derivePoolSecret with different salt returns different output', async () => {
|
|
205
|
+
// Modify salt in a copy of config
|
|
206
|
+
const configB = { ...config, salt: "OtherPool" };
|
|
207
|
+
const strategyA = KeystoneStrategyFactory.create({ config });
|
|
208
|
+
const strategyB = KeystoneStrategyFactory.create({ config: configB });
|
|
209
|
+
|
|
210
|
+
const secretA = await strategyA.derivePoolSecret({ masterSecret });
|
|
211
|
+
const secretB = await strategyB.derivePoolSecret({ masterSecret });
|
|
212
|
+
|
|
213
|
+
iReckon(sir, secretA).asTo('salt affects secret').not.willEqual(secretB);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await ifWeMight(sir, 'deriveDelegateSecret derives a consistent secret using KDF', async () => {
|
|
217
|
+
const secretA = await deriveDelegateSecret({ masterSecret });
|
|
218
|
+
const secretB = await deriveDelegateSecret({ masterSecret });
|
|
219
|
+
|
|
220
|
+
iReckon(sir, secretA).asTo('consistency').willEqual(secretB);
|
|
221
|
+
iReckon(sir, secretA).asTo('truthy').isGonnaBeTruthy();
|
|
222
|
+
|
|
223
|
+
const secretC = await deriveDelegateSecret({ masterSecret: masterSecret + '_other' });
|
|
224
|
+
iReckon(sir, secretA).asTo('differ').not.willEqual(secretC);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
await respecfully(sir, 'Challenge/Solution Logic', async () => {
|
|
229
|
+
|
|
230
|
+
await ifWeMight(sir, 'generateSolution -> generateChallenge -> validateSolution loop works', async () => {
|
|
231
|
+
const strategy = KeystoneStrategyFactory.create({ config });
|
|
232
|
+
const poolSecret = await strategy.derivePoolSecret({ masterSecret });
|
|
233
|
+
const challengeId = "a3ff7843552870fc28bef2b"; // arbitrary random challengeId
|
|
234
|
+
|
|
235
|
+
// 1. Generate Solution
|
|
236
|
+
const solution = await strategy.generateSolution({ poolSecret, poolId: config.id, challengeId });
|
|
237
|
+
iReckon(sir, solution.value).asTo('solution value exists').isGonnaBeTruthy();
|
|
238
|
+
iReckon(sir, solution.challengeId).asTo('id matches').willEqual(challengeId);
|
|
239
|
+
|
|
240
|
+
// 2. Generate Public Challenge from Solution
|
|
241
|
+
const challenge = await strategy.generateChallenge({ solution });
|
|
242
|
+
iReckon(sir, challenge.hash).asTo('challenge hash exists').isGonnaBeTruthy();
|
|
243
|
+
|
|
244
|
+
// 3. Validate
|
|
245
|
+
const isValid = await strategy.validateSolution({ solution, challenge });
|
|
246
|
+
iReckon(sir, isValid).asTo('valid pair should pass').isGonnaBeTrue();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await ifWeMight(sir, 'validateSolution fails for mismatched values', async () => {
|
|
250
|
+
const strategy = KeystoneStrategyFactory.create({ config });
|
|
251
|
+
const poolSecret = await strategy.derivePoolSecret({ masterSecret });
|
|
252
|
+
const challengeId = "8c994f3ed598f150e25513"; // arbitrary random challengeId
|
|
253
|
+
|
|
254
|
+
// Generate real pair
|
|
255
|
+
const solution = await strategy.generateSolution({ poolSecret, poolId: config.id, challengeId });
|
|
256
|
+
const challenge = await strategy.generateChallenge({ solution });
|
|
257
|
+
|
|
258
|
+
// Tamper with solution value
|
|
259
|
+
const badSolution = { ...solution, value: "hacked_value" };
|
|
260
|
+
|
|
261
|
+
const isValid = await strategy.validateSolution({ solution: badSolution, challenge });
|
|
262
|
+
iReckon(sir, isValid).asTo('tampered solution should fail').isGonnaBeFalse();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await ifWeMight(sir, 'validateSolution fails for mismatched challenge hashes', async () => {
|
|
266
|
+
const strategy = KeystoneStrategyFactory.create({ config });
|
|
267
|
+
const poolSecret = await strategy.derivePoolSecret({ masterSecret });
|
|
268
|
+
|
|
269
|
+
// Generate pair A
|
|
270
|
+
const challengeId_A = "416c38cfd6ee63dbf8d4e5ef36"; // arbitrary random challengeId
|
|
271
|
+
const solutionA = await strategy.generateSolution({ poolSecret, poolId: config.id, challengeId: challengeId_A });
|
|
272
|
+
|
|
273
|
+
// Generate pair B
|
|
274
|
+
const challengeId_B = "c487ef6b7878fae798c3"; // arbitrary random challengeId
|
|
275
|
+
const solutionB = await strategy.generateSolution({ poolSecret, poolId: config.id, challengeId: challengeId_B });
|
|
276
|
+
const challengeB = await strategy.generateChallenge({ solution: solutionB });
|
|
277
|
+
|
|
278
|
+
// Check A against B
|
|
279
|
+
const isValid = await strategy.validateSolution({ solution: solutionA, challenge: challengeB });
|
|
280
|
+
iReckon(sir, isValid).asTo('mismatched pair should fail').isGonnaBeFalse();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// ===========================================================================
|
|
286
|
+
// SUITE B: SERVICE LIFECYCLE (Genesis -> Sign -> Validate)
|
|
287
|
+
// ===========================================================================
|
|
288
|
+
|
|
289
|
+
await respecfully(sir, 'Suite B: Service Lifecycle', async () => {
|
|
290
|
+
|
|
291
|
+
const service = new KeystoneService_V1();
|
|
292
|
+
const masterSecret = "AliceSecretKey_987654321";
|
|
293
|
+
|
|
294
|
+
let mockSpace: MockIbGibSpace;
|
|
295
|
+
let mockMetaspace: any;
|
|
296
|
+
|
|
297
|
+
let genesisKeystone: KeystoneIbGib_V1;
|
|
298
|
+
let signedKeystone: KeystoneIbGib_V1;
|
|
299
|
+
|
|
300
|
+
firstOfAll(sir, async () => {
|
|
301
|
+
mockSpace = new MockIbGibSpace();
|
|
302
|
+
mockMetaspace = new MockMetaspaceService(mockSpace);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
await respecfully(sir, 'Genesis', async () => {
|
|
306
|
+
await ifWeMight(sir, 'creates a valid genesis frame and persists it', async () => {
|
|
307
|
+
const salt = (await getUUID()).substring(0, 16);
|
|
308
|
+
const config = createStandardPoolConfig({
|
|
309
|
+
id: POOL_ID_DEFAULT,
|
|
310
|
+
salt,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
genesisKeystone = await service.genesis({
|
|
314
|
+
masterSecret,
|
|
315
|
+
configs: [config],
|
|
316
|
+
metaspace: mockMetaspace,
|
|
317
|
+
space: mockSpace as any,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Verify Object
|
|
321
|
+
iReckon(sir, genesisKeystone).asTo('genesis object').isGonnaBeTruthy();
|
|
322
|
+
iReckon(sir, genesisKeystone.data?.isTjp).asTo('isTjp').isGonnaBeTrue();
|
|
323
|
+
|
|
324
|
+
// Verify Persistence
|
|
325
|
+
const addr = getIbGibAddr({ ibGib: genesisKeystone });
|
|
326
|
+
const saved = await mockSpace.get({ addr });
|
|
327
|
+
|
|
328
|
+
iReckon(sir, saved).asTo('persisted to space').isGonnaBeTruthy();
|
|
329
|
+
|
|
330
|
+
// Verify Registration (Timeline Tracking)
|
|
331
|
+
// Genesis gib should be registered as a timeline head
|
|
332
|
+
const head = mockMetaspace.timelineHeads.get(genesisKeystone.gib!);
|
|
333
|
+
iReckon(sir, head).asTo('genesis registered as timeline head').willEqual(addr);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
await respecfully(sir, 'Signing (Evolution)', async () => {
|
|
338
|
+
await ifWeMight(sir, 'evolves the keystone with a valid proof', async () => {
|
|
339
|
+
const claim: Partial<KeystoneClaim> = {
|
|
340
|
+
target: "comment 123^gib",
|
|
341
|
+
verb: "post"
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const details = { note: "First post!" };
|
|
345
|
+
|
|
346
|
+
signedKeystone = await service.sign({
|
|
347
|
+
latestKeystone: genesisKeystone,
|
|
348
|
+
masterSecret,
|
|
349
|
+
claim,
|
|
350
|
+
poolId: POOL_ID_DEFAULT,
|
|
351
|
+
frameDetails: details,
|
|
352
|
+
metaspace: mockMetaspace,
|
|
353
|
+
space: mockSpace as any,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
iReckon(sir, signedKeystone).asTo('new frame created').isGonnaBeTruthy();
|
|
357
|
+
iReckon(sir, signedKeystone).asTo('is different frame').not.isGonnaBe(genesisKeystone);
|
|
358
|
+
|
|
359
|
+
// NOTE: If this fails, check if 'sign' calls 'space.put' or 'metaspace.put'!
|
|
360
|
+
// In your current 'sign' implementation, you return the object but might have missed the save step.
|
|
361
|
+
const addr = getIbGibAddr({ ibGib: signedKeystone });
|
|
362
|
+
const saved = await mockSpace.get({ addr });
|
|
363
|
+
iReckon(sir, saved).asTo('persisted to space').isGonnaBeTruthy();
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
await respecfully(sir, 'Validation', async () => {
|
|
368
|
+
await ifWeMight(sir, 'validates the genesis->signed transition', async () => {
|
|
369
|
+
const errors = await service.validate({
|
|
370
|
+
prevIbGib: genesisKeystone,
|
|
371
|
+
currentIbGib: signedKeystone,
|
|
372
|
+
// metaspace: mockMetaspace,
|
|
373
|
+
// space: mockSpace as any,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
iReckon(sir, errors.length).asTo('signature validation has no errors').willEqual(0);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// ===========================================================================
|
|
382
|
+
// SUITE C: SECURITY & SAD PATHS
|
|
383
|
+
// ===========================================================================
|
|
384
|
+
|
|
385
|
+
await respecfully(sir, 'Suite C: Security Vectors', async () => {
|
|
386
|
+
|
|
387
|
+
const service = new KeystoneService_V1();
|
|
388
|
+
const aliceSecret = "AliceSecret_111";
|
|
389
|
+
const eveSecret = "EveSecret_666";
|
|
390
|
+
|
|
391
|
+
let mockSpace: MockIbGibSpace;
|
|
392
|
+
let mockMetaspace: any;
|
|
393
|
+
let genesisKeystone: KeystoneIbGib_V1;
|
|
394
|
+
|
|
395
|
+
firstOfAll(sir, async () => {
|
|
396
|
+
mockSpace = new MockIbGibSpace();
|
|
397
|
+
mockMetaspace = new MockMetaspaceService(mockSpace);
|
|
398
|
+
|
|
399
|
+
// Setup Alice's Identity
|
|
400
|
+
const salt = (await getUUID()).substring(0, 16);
|
|
401
|
+
const config = createStandardPoolConfig({
|
|
402
|
+
id: POOL_ID_DEFAULT,
|
|
403
|
+
salt,
|
|
404
|
+
targetBinding: 0,
|
|
405
|
+
size: 10,
|
|
406
|
+
});
|
|
407
|
+
// config.behavior.size = 10;
|
|
408
|
+
genesisKeystone = await service.genesis({
|
|
409
|
+
masterSecret: aliceSecret,
|
|
410
|
+
configs: [config],
|
|
411
|
+
metaspace: mockMetaspace,
|
|
412
|
+
space: mockSpace as any,
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
await respecfully(sir, 'Wrong Secret (Forgery)', async () => {
|
|
417
|
+
await ifWeMight(sir, 'prevents creation of forged frames', async () => {
|
|
418
|
+
const claim: Partial<KeystoneClaim> = { target: "comment 123^gib", verb: "post" };
|
|
419
|
+
|
|
420
|
+
let errorCaught = false;
|
|
421
|
+
let errorMsg = "";
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
// Eve tries to sign Alice's keystone.
|
|
425
|
+
// This MUST fail because sign() calls evolve(), which calls validate().
|
|
426
|
+
await service.sign({
|
|
427
|
+
latestKeystone: genesisKeystone,
|
|
428
|
+
masterSecret: eveSecret,
|
|
429
|
+
claim,
|
|
430
|
+
poolId: POOL_ID_DEFAULT,
|
|
431
|
+
metaspace: mockMetaspace,
|
|
432
|
+
space: mockSpace as any,
|
|
433
|
+
});
|
|
434
|
+
} catch (e: any) {
|
|
435
|
+
errorCaught = true;
|
|
436
|
+
errorMsg = e.message;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
iReckon(sir, errorCaught).asTo('service rejected forgery').isGonnaBeTrue();
|
|
440
|
+
// Verify it was a crypto error, not something else
|
|
441
|
+
iReckon(sir, errorMsg).asTo('error mentions crypto violation').includes('Crypto Violation');
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
await respecfully(sir, 'Policy Violation (Restricted Verbs)', async () => {
|
|
446
|
+
await ifWeMight(sir, 'throws error if signing forbidden verb with restricted pool', async () => {
|
|
447
|
+
// Create a specific restricted pool config manually
|
|
448
|
+
const restrictedPoolId = "read_only_pool";
|
|
449
|
+
const restrictedConfig = createStandardPoolConfig({
|
|
450
|
+
id: restrictedPoolId,
|
|
451
|
+
salt: restrictedPoolId,
|
|
452
|
+
});
|
|
453
|
+
// Manually restrict it (since Builder defaults to undefined/allow-all)
|
|
454
|
+
restrictedConfig.allowedVerbs = ['read'];
|
|
455
|
+
|
|
456
|
+
const restrictedGenesis = await service.genesis({
|
|
457
|
+
masterSecret: aliceSecret,
|
|
458
|
+
configs: [restrictedConfig],
|
|
459
|
+
metaspace: mockMetaspace,
|
|
460
|
+
space: mockSpace as any,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Try to sign "write" using "read_only_pool"
|
|
464
|
+
const claim: Partial<KeystoneClaim> = { target: "data^gib", verb: "write" };
|
|
465
|
+
|
|
466
|
+
let errorCaught = false;
|
|
467
|
+
try {
|
|
468
|
+
await service.sign({
|
|
469
|
+
latestKeystone: restrictedGenesis,
|
|
470
|
+
masterSecret: aliceSecret,
|
|
471
|
+
claim,
|
|
472
|
+
poolId: restrictedPoolId, // Force use of restricted pool
|
|
473
|
+
metaspace: mockMetaspace,
|
|
474
|
+
space: mockSpace as any,
|
|
475
|
+
});
|
|
476
|
+
} catch (e) {
|
|
477
|
+
errorCaught = true;
|
|
478
|
+
// Optional: Check error message contains "not authorized"
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
iReckon(sir, errorCaught).asTo('policy enforced').isGonnaBeTrue();
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// ===========================================================================
|
|
487
|
+
// SUITE D: REVOCATION
|
|
488
|
+
// ===========================================================================
|
|
489
|
+
|
|
490
|
+
await respecfully(sir, 'Suite D: Revocation', async () => {
|
|
491
|
+
|
|
492
|
+
const service = new KeystoneService_V1();
|
|
493
|
+
const masterSecret = "AliceSecret_RevokeTest";
|
|
494
|
+
|
|
495
|
+
let mockSpace: MockIbGibSpace;
|
|
496
|
+
let mockMetaspace: any;
|
|
497
|
+
let genesisKeystone: KeystoneIbGib_V1;
|
|
498
|
+
|
|
499
|
+
firstOfAll(sir, async () => {
|
|
500
|
+
mockSpace = new MockIbGibSpace();
|
|
501
|
+
mockMetaspace = new MockMetaspaceService(mockSpace);
|
|
502
|
+
|
|
503
|
+
// Setup Identity WITH a Revocation Pool
|
|
504
|
+
const stdSalt = (await getUUID()).substring(0, 16);
|
|
505
|
+
const revokeSalt = (await getUUID()).substring(0, 16);
|
|
506
|
+
const stdConfig = createStandardPoolConfig({
|
|
507
|
+
id: POOL_ID_DEFAULT,
|
|
508
|
+
salt: stdSalt,
|
|
509
|
+
});
|
|
510
|
+
const revokeConfig = createRevocationPoolConfig({
|
|
511
|
+
id: POOL_ID_REVOKE,
|
|
512
|
+
salt: revokeSalt,
|
|
513
|
+
}); // Special Config
|
|
514
|
+
|
|
515
|
+
genesisKeystone = await service.genesis({
|
|
516
|
+
masterSecret,
|
|
517
|
+
configs: [stdConfig, revokeConfig],
|
|
518
|
+
metaspace: mockMetaspace,
|
|
519
|
+
space: mockSpace as any,
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
await respecfully(sir, 'Revoke Lifecycle', async () => {
|
|
524
|
+
let revokedKeystone: KeystoneIbGib_V1;
|
|
525
|
+
|
|
526
|
+
await ifWeMight(sir, 'successfully creates a revocation frame', async () => {
|
|
527
|
+
revokedKeystone = await service.revoke({
|
|
528
|
+
latestKeystone: genesisKeystone,
|
|
529
|
+
masterSecret,
|
|
530
|
+
reason: "Key compromised",
|
|
531
|
+
metaspace: mockMetaspace,
|
|
532
|
+
space: mockSpace as any,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
iReckon(sir, revokedKeystone).isGonnaBeTruthy();
|
|
536
|
+
|
|
537
|
+
// Check Data
|
|
538
|
+
const data = revokedKeystone.data!;
|
|
539
|
+
iReckon(sir, data.revocationInfo).asTo('revocation info present').isGonnaBeTruthy();
|
|
540
|
+
iReckon(sir, data.revocationInfo!.reason).willEqual("Key compromised");
|
|
541
|
+
iReckon(sir, data.revocationInfo!.proof.claim.verb).willEqual(KEYSTONE_VERB_REVOKE);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
await ifWeMight(sir, 'validates the revocation frame', async () => {
|
|
545
|
+
const errors = await service.validate({
|
|
546
|
+
prevIbGib: genesisKeystone,
|
|
547
|
+
currentIbGib: revokedKeystone!,
|
|
548
|
+
// metaspace: mockMetaspace,
|
|
549
|
+
// space: mockSpace as any,
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
iReckon(sir, errors.length).asTo('no validation errors').willEqual(0);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
await ifWeMight(sir, 'consumed the revocation pool (Scorched Earth)', async () => {
|
|
556
|
+
const data = revokedKeystone!.data!;
|
|
557
|
+
const revokePool = data.challengePools.find(p => p.id === POOL_ID_REVOKE);
|
|
558
|
+
|
|
559
|
+
// The pool should exist...
|
|
560
|
+
iReckon(sir, revokePool).isGonnaBeTruthy();
|
|
561
|
+
|
|
562
|
+
// Should be empty (0 challenges)
|
|
563
|
+
const remaining = Object.keys(revokePool!.challenges);
|
|
564
|
+
iReckon(sir, remaining.length).asTo('pool depleted').willEqual(0);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
await ifWeMight(sir, 'fails to sign any new claims after revocation', async () => {
|
|
568
|
+
let errorCaught = false;
|
|
569
|
+
try {
|
|
570
|
+
await service.sign({
|
|
571
|
+
latestKeystone: revokedKeystone,
|
|
572
|
+
masterSecret,
|
|
573
|
+
claim: { verb: "post", target: "some_target" },
|
|
574
|
+
metaspace: mockMetaspace,
|
|
575
|
+
space: mockSpace as any,
|
|
576
|
+
});
|
|
577
|
+
} catch (err: any) {
|
|
578
|
+
errorCaught = true;
|
|
579
|
+
iReckon(sir, err.message).includes("has been revoked");
|
|
580
|
+
}
|
|
581
|
+
iReckon(sir, errorCaught).asTo('signing blocked after revocation').isGonnaBeTrue();
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
await ifWeMight(sir, 'fails to evolve or add pools after revocation', async () => {
|
|
585
|
+
let errorCaught = false;
|
|
586
|
+
try {
|
|
587
|
+
const dummyConfig = createStandardPoolConfig({ id: "dummy", salt: "dummy" });
|
|
588
|
+
const dummyPool = { id: "dummy", config: dummyConfig, challenges: {} };
|
|
589
|
+
await service.addPools({
|
|
590
|
+
latestKeystone: revokedKeystone,
|
|
591
|
+
masterSecret,
|
|
592
|
+
newPools: [dummyPool],
|
|
593
|
+
metaspace: mockMetaspace,
|
|
594
|
+
space: mockSpace as any,
|
|
595
|
+
});
|
|
596
|
+
} catch (err: any) {
|
|
597
|
+
errorCaught = true;
|
|
598
|
+
iReckon(sir, err.message).includes("has been revoked");
|
|
599
|
+
}
|
|
600
|
+
iReckon(sir, errorCaught).asTo('adding pools blocked after revocation').isGonnaBeTrue();
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// ===========================================================================
|
|
606
|
+
// SUITE E: STRUCTURAL EVOLUTION (addPools)
|
|
607
|
+
// ===========================================================================
|
|
608
|
+
|
|
609
|
+
await respecfully(sir, 'Suite E: Structural Evolution (addPools)', async () => {
|
|
610
|
+
|
|
611
|
+
const service = new KeystoneService_V1();
|
|
612
|
+
const aliceSecret = "Alice_Master_Key";
|
|
613
|
+
const bobSecret = "Bob_Foreign_Key";
|
|
614
|
+
|
|
615
|
+
let mockSpace: MockIbGibSpace;
|
|
616
|
+
let mockMetaspace: any;
|
|
617
|
+
let aliceKeystone: KeystoneIbGib_V1;
|
|
618
|
+
|
|
619
|
+
// Helper to generate a "Foreign" pool (e.g. from Bob)
|
|
620
|
+
const createForeignPool = async (id: string, verbs: string[] = []): Promise<KeystoneChallengePool> => {
|
|
621
|
+
const salt = (await getUUID()).substring(0, 16);
|
|
622
|
+
const config = createStandardPoolConfig({ id, salt });
|
|
623
|
+
config.allowedVerbs = verbs;
|
|
624
|
+
|
|
625
|
+
// We use the factory manually here to simulate Bob doing this offline
|
|
626
|
+
const strategy = KeystoneStrategyFactory.create({ config });
|
|
627
|
+
const poolSecret = await strategy.derivePoolSecret({ masterSecret: bobSecret });
|
|
628
|
+
const challenges: { [id: string]: any } = {};
|
|
629
|
+
|
|
630
|
+
for (let i = 0; i < 10; i++) {
|
|
631
|
+
// Manually replicate ID gen for test
|
|
632
|
+
const raw = await hash({ s: `${config.salt}${Date.now()}${i}` });
|
|
633
|
+
const challengeId = raw.substring(0, 16);
|
|
634
|
+
|
|
635
|
+
const solution = await strategy.generateSolution({
|
|
636
|
+
poolSecret, poolId: config.salt, challengeId,
|
|
637
|
+
});
|
|
638
|
+
const challenge = await strategy.generateChallenge({ solution });
|
|
639
|
+
challenges[challengeId] = challenge;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return {
|
|
643
|
+
id,
|
|
644
|
+
config,
|
|
645
|
+
challenges,
|
|
646
|
+
isForeign: true,
|
|
647
|
+
metadata: { owner: 'Bob' }
|
|
648
|
+
};
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
firstOfAll(sir, async () => {
|
|
652
|
+
mockSpace = new MockIbGibSpace();
|
|
653
|
+
mockMetaspace = new MockMetaspaceService(mockSpace);
|
|
654
|
+
|
|
655
|
+
// Alice Genesis: Standard pool (allows all verbs, including 'manage')
|
|
656
|
+
const salt = (await getUUID()).substring(0, 16);
|
|
657
|
+
const config = createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt });
|
|
658
|
+
aliceKeystone = await service.genesis({
|
|
659
|
+
masterSecret: aliceSecret,
|
|
660
|
+
configs: [config],
|
|
661
|
+
metaspace: mockMetaspace,
|
|
662
|
+
space: mockSpace as any,
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
await respecfully(sir, 'Happy Path', async () => {
|
|
667
|
+
await ifWeMight(sir, 'authorizes and adds a foreign pool', async () => {
|
|
668
|
+
const bobPool = await createForeignPool("pool_bob", ["post"]);
|
|
669
|
+
|
|
670
|
+
const updatedKeystone = await service.addPools({
|
|
671
|
+
latestKeystone: aliceKeystone,
|
|
672
|
+
masterSecret: aliceSecret,
|
|
673
|
+
newPools: [bobPool],
|
|
674
|
+
metaspace: mockMetaspace,
|
|
675
|
+
space: mockSpace as any,
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// 1. Verify new state
|
|
679
|
+
iReckon(sir, updatedKeystone).isGonnaBeTruthy();
|
|
680
|
+
const pools = updatedKeystone.data!.challengePools;
|
|
681
|
+
iReckon(sir, pools.length).asTo('pool count increased').willEqual(2);
|
|
682
|
+
|
|
683
|
+
const foundBob = pools.find(p => p.id === "pool_bob");
|
|
684
|
+
iReckon(sir, foundBob).asTo('bob pool exists').isGonnaBeTruthy();
|
|
685
|
+
iReckon(sir, foundBob!.isForeign).asTo('isForeign flag').isGonnaBeTrue();
|
|
686
|
+
|
|
687
|
+
// 2. Verify Proof
|
|
688
|
+
const proof = updatedKeystone.data!.proofs[0];
|
|
689
|
+
iReckon(sir, proof.claim.verb).asTo('proof verb').willEqual("manage");
|
|
690
|
+
// Alice signed this using HER pool (default), not Bob's
|
|
691
|
+
iReckon(sir, proof.solutions[0].poolId).willEqual(POOL_ID_DEFAULT);
|
|
692
|
+
|
|
693
|
+
// 3. Verify Validity
|
|
694
|
+
const errors = await service.validate({
|
|
695
|
+
prevIbGib: aliceKeystone,
|
|
696
|
+
currentIbGib: updatedKeystone,
|
|
697
|
+
});
|
|
698
|
+
iReckon(sir, errors.length).asTo('validation passed').willEqual(0);
|
|
699
|
+
|
|
700
|
+
// Update local ref for next tests
|
|
701
|
+
aliceKeystone = updatedKeystone;
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
await respecfully(sir, 'Permissions & Logic', async () => {
|
|
706
|
+
await ifWeMight(sir, 'fails if no pool allows "manage" verb', async () => {
|
|
707
|
+
// 1. Create a restricted keystone
|
|
708
|
+
let id = "read_only";
|
|
709
|
+
const restrictedConfig = createStandardPoolConfig({ id, salt: id });
|
|
710
|
+
restrictedConfig.allowedVerbs = ['read']; // No 'manage'
|
|
711
|
+
|
|
712
|
+
const restrictedKeystone = await service.genesis({
|
|
713
|
+
masterSecret: aliceSecret,
|
|
714
|
+
configs: [restrictedConfig],
|
|
715
|
+
metaspace: mockMetaspace,
|
|
716
|
+
space: mockSpace as any,
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
const newPool = await createForeignPool("pool_test");
|
|
720
|
+
let errorCaught = false;
|
|
721
|
+
|
|
722
|
+
try {
|
|
723
|
+
await service.addPools({
|
|
724
|
+
latestKeystone: restrictedKeystone,
|
|
725
|
+
masterSecret: aliceSecret,
|
|
726
|
+
newPools: [newPool],
|
|
727
|
+
metaspace: mockMetaspace,
|
|
728
|
+
space: mockSpace as any,
|
|
729
|
+
});
|
|
730
|
+
} catch (e: any) {
|
|
731
|
+
errorCaught = true;
|
|
732
|
+
// Should fail in resolveTargetPool or addPools logic
|
|
733
|
+
// console.log("Caught expected error:", e.message);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
iReckon(sir, errorCaught).asTo('permission denied').isGonnaBeTrue();
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
await ifWeMight(sir, 'fails on ID collision', async () => {
|
|
740
|
+
// Try to add "pool_bob" again (it was added in Happy Path)
|
|
741
|
+
const duplicatePool = await createForeignPool("pool_bob");
|
|
742
|
+
|
|
743
|
+
let errorCaught = false;
|
|
744
|
+
try {
|
|
745
|
+
await service.addPools({
|
|
746
|
+
latestKeystone: aliceKeystone, // This already has pool_bob
|
|
747
|
+
masterSecret: aliceSecret,
|
|
748
|
+
newPools: [duplicatePool],
|
|
749
|
+
metaspace: mockMetaspace,
|
|
750
|
+
space: mockSpace as any,
|
|
751
|
+
});
|
|
752
|
+
} catch (e: any) {
|
|
753
|
+
errorCaught = true;
|
|
754
|
+
iReckon(sir, e.message).includes("ID collision");
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
iReckon(sir, errorCaught).asTo('collision detected').isGonnaBeTrue();
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
// ===========================================================================
|
|
763
|
+
// SUITE E: STRUCTURAL EVOLUTION (addPools)
|
|
764
|
+
// ===========================================================================
|
|
765
|
+
|
|
766
|
+
await respecfully(sir, 'Suite E: Structural Evolution (addPools)', async () => {
|
|
767
|
+
|
|
768
|
+
const service = new KeystoneService_V1();
|
|
769
|
+
const aliceSecret = "Alice_Master_Key";
|
|
770
|
+
const bobSecret = "Bob_Foreign_Key";
|
|
771
|
+
|
|
772
|
+
let mockSpace: MockIbGibSpace;
|
|
773
|
+
let mockMetaspace: any;
|
|
774
|
+
let aliceKeystone: KeystoneIbGib_V1;
|
|
775
|
+
|
|
776
|
+
// Helper to simulate Bob creating a pool "offline" to give to Alice
|
|
777
|
+
const createForeignPool = async (id: string, verbs: string[] = []): Promise<KeystoneChallengePool> => {
|
|
778
|
+
const salt = (await getUUID()).substring(0, 16);
|
|
779
|
+
const config = createStandardPoolConfig({ id, salt });
|
|
780
|
+
config.allowedVerbs = verbs;
|
|
781
|
+
|
|
782
|
+
// We use the factory manually here to simulate Bob doing this on his own machine
|
|
783
|
+
const strategy = KeystoneStrategyFactory.create({ config });
|
|
784
|
+
const poolSecret = await strategy.derivePoolSecret({ masterSecret: bobSecret });
|
|
785
|
+
const challenges: { [id: string]: any } = {};
|
|
786
|
+
|
|
787
|
+
// Generate a small set of challenges
|
|
788
|
+
for (let i = 0; i < 10; i++) {
|
|
789
|
+
const raw = await hash({ s: `${config.salt}${Date.now()}${i}` });
|
|
790
|
+
const challengeId = raw.substring(0, 16);
|
|
791
|
+
|
|
792
|
+
const solution = await strategy.generateSolution({
|
|
793
|
+
poolSecret, poolId: config.salt, challengeId,
|
|
794
|
+
});
|
|
795
|
+
const challenge = await strategy.generateChallenge({ solution });
|
|
796
|
+
challenges[challengeId] = challenge;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return {
|
|
800
|
+
id,
|
|
801
|
+
config,
|
|
802
|
+
challenges,
|
|
803
|
+
isForeign: true,
|
|
804
|
+
metadata: { owner: 'Bob', role: 'Delegate' }
|
|
805
|
+
};
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
firstOfAll(sir, async () => {
|
|
809
|
+
mockSpace = new MockIbGibSpace();
|
|
810
|
+
mockMetaspace = new MockMetaspaceService(mockSpace);
|
|
811
|
+
|
|
812
|
+
// Alice Genesis: Standard pool (allows all verbs, including 'manage')
|
|
813
|
+
const salt = (await getUUID()).substring(0, 16);
|
|
814
|
+
const config = createStandardPoolConfig({
|
|
815
|
+
id: POOL_ID_DEFAULT,
|
|
816
|
+
salt,
|
|
817
|
+
});
|
|
818
|
+
aliceKeystone = await service.genesis({
|
|
819
|
+
masterSecret: aliceSecret,
|
|
820
|
+
configs: [config],
|
|
821
|
+
metaspace: mockMetaspace,
|
|
822
|
+
space: mockSpace as any,
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
await respecfully(sir, 'Happy Path', async () => {
|
|
827
|
+
await ifWeMight(sir, 'authorizes and adds a foreign pool', async () => {
|
|
828
|
+
const bobPool = await createForeignPool("pool_bob", ["post"]);
|
|
829
|
+
|
|
830
|
+
const updatedKeystone = await service.addPools({
|
|
831
|
+
latestKeystone: aliceKeystone,
|
|
832
|
+
masterSecret: aliceSecret,
|
|
833
|
+
newPools: [bobPool],
|
|
834
|
+
metaspace: mockMetaspace,
|
|
835
|
+
space: mockSpace as any,
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
// 1. Verify new state
|
|
839
|
+
iReckon(sir, updatedKeystone).isGonnaBeTruthy();
|
|
840
|
+
const pools = updatedKeystone.data!.challengePools;
|
|
841
|
+
iReckon(sir, pools.length).asTo('pool count increased').willEqual(2);
|
|
842
|
+
|
|
843
|
+
const foundBob = pools.find(p => p.id === "pool_bob");
|
|
844
|
+
iReckon(sir, foundBob).asTo('bob pool exists').isGonnaBeTruthy();
|
|
845
|
+
iReckon(sir, foundBob!.isForeign).asTo('isForeign flag').isGonnaBeTrue();
|
|
846
|
+
|
|
847
|
+
// 2. Verify Proof
|
|
848
|
+
const proof = updatedKeystone.data!.proofs[0];
|
|
849
|
+
iReckon(sir, proof.claim.verb).asTo('proof verb').willEqual(KEYSTONE_VERB_MANAGE);
|
|
850
|
+
// Alice signed this using HER pool (default), not Bob's
|
|
851
|
+
iReckon(sir, proof.solutions[0].poolId).willEqual(POOL_ID_DEFAULT);
|
|
852
|
+
|
|
853
|
+
// 3. Verify Validity
|
|
854
|
+
const errors = await service.validate({
|
|
855
|
+
prevIbGib: aliceKeystone,
|
|
856
|
+
currentIbGib: updatedKeystone,
|
|
857
|
+
});
|
|
858
|
+
iReckon(sir, errors.length).asTo('validation passed').willEqual(0);
|
|
859
|
+
|
|
860
|
+
// Update local ref for next tests
|
|
861
|
+
aliceKeystone = updatedKeystone;
|
|
862
|
+
});
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
await respecfully(sir, 'Permissions & Logic', async () => {
|
|
866
|
+
await ifWeMight(sir, 'fails if no pool allows "manage" verb', async () => {
|
|
867
|
+
// 1. Create a restricted keystone (read-only)
|
|
868
|
+
let id = "read_only";
|
|
869
|
+
const restrictedConfig = createStandardPoolConfig({ id, salt: id });
|
|
870
|
+
restrictedConfig.allowedVerbs = ['read']; // No 'manage'
|
|
871
|
+
|
|
872
|
+
const restrictedKeystone = await service.genesis({
|
|
873
|
+
masterSecret: aliceSecret,
|
|
874
|
+
configs: [restrictedConfig],
|
|
875
|
+
metaspace: mockMetaspace,
|
|
876
|
+
space: mockSpace as any,
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
const newPool = await createForeignPool("pool_test");
|
|
880
|
+
let errorCaught = false;
|
|
881
|
+
|
|
882
|
+
try {
|
|
883
|
+
await service.addPools({
|
|
884
|
+
latestKeystone: restrictedKeystone,
|
|
885
|
+
masterSecret: aliceSecret,
|
|
886
|
+
newPools: [newPool],
|
|
887
|
+
metaspace: mockMetaspace,
|
|
888
|
+
space: mockSpace as any,
|
|
889
|
+
});
|
|
890
|
+
} catch (e: any) {
|
|
891
|
+
errorCaught = true;
|
|
892
|
+
// Optional: Check error message
|
|
893
|
+
// iReckon(sir, e.message).includes("No local pool found with 'manage'");
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
iReckon(sir, errorCaught).asTo('permission denied').isGonnaBeTrue();
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
await ifWeMight(sir, 'fails on ID collision', async () => {
|
|
900
|
+
// Try to add "pool_bob" again (it was added in Happy Path)
|
|
901
|
+
const duplicatePool = await createForeignPool("pool_bob");
|
|
902
|
+
|
|
903
|
+
let errorCaught = false;
|
|
904
|
+
try {
|
|
905
|
+
await service.addPools({
|
|
906
|
+
latestKeystone: aliceKeystone, // This already has pool_bob
|
|
907
|
+
masterSecret: aliceSecret,
|
|
908
|
+
newPools: [duplicatePool],
|
|
909
|
+
metaspace: mockMetaspace,
|
|
910
|
+
space: mockSpace as any,
|
|
911
|
+
});
|
|
912
|
+
} catch (e: any) {
|
|
913
|
+
errorCaught = true;
|
|
914
|
+
iReckon(sir, e.message).includes("collision");
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
iReckon(sir, errorCaught).asTo('collision detected').isGonnaBeTrue();
|
|
918
|
+
});
|
|
919
|
+
});
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
// ===========================================================================
|
|
923
|
+
// SUITE F: DEEP INSPECTION (Granularity & Serialization)
|
|
924
|
+
// ===========================================================================
|
|
925
|
+
|
|
926
|
+
await respecfully(sir, 'Suite F: Deep Inspection', async () => {
|
|
927
|
+
|
|
928
|
+
const service = new KeystoneService_V1();
|
|
929
|
+
const aliceSecret = "Alice_Deep_Inspect";
|
|
930
|
+
const poolId = "granularity_pool";
|
|
931
|
+
|
|
932
|
+
let mockSpace: MockIbGibSpace;
|
|
933
|
+
let mockMetaspace: any;
|
|
934
|
+
let genesisKeystone: KeystoneIbGib_V1;
|
|
935
|
+
let signedKeystone: KeystoneIbGib_V1;
|
|
936
|
+
let hybridConfig: KeystonePoolConfig_HashV1;
|
|
937
|
+
|
|
938
|
+
firstOfAll(sir, async () => {
|
|
939
|
+
mockSpace = new MockIbGibSpace();
|
|
940
|
+
mockMetaspace = new MockMetaspaceService(mockSpace);
|
|
941
|
+
|
|
942
|
+
const salt = (await getUUID()).substring(0, 16);
|
|
943
|
+
hybridConfig = createStandardPoolConfig({
|
|
944
|
+
id: poolId,
|
|
945
|
+
salt,
|
|
946
|
+
// 2 FIFO + 2 Random = 4 Total per sign
|
|
947
|
+
sequential: 2, random: 2, targetBinding: 0,
|
|
948
|
+
size: 20, // Small enough to track, large enough to be random
|
|
949
|
+
}) as KeystonePoolConfig_HashV1;
|
|
950
|
+
|
|
951
|
+
genesisKeystone = await service.genesis({
|
|
952
|
+
masterSecret: aliceSecret,
|
|
953
|
+
configs: [hybridConfig],
|
|
954
|
+
metaspace: mockMetaspace,
|
|
955
|
+
space: mockSpace as any,
|
|
956
|
+
});
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
await respecfully(sir, 'Proof Granularity & Math', async () => {
|
|
960
|
+
|
|
961
|
+
await ifWeMight(sir, 'generates exactly the expected number of solutions', async () => {
|
|
962
|
+
signedKeystone = await service.sign({
|
|
963
|
+
latestKeystone: genesisKeystone,
|
|
964
|
+
masterSecret: aliceSecret,
|
|
965
|
+
claim: { verb: "post", target: "data^gib" },
|
|
966
|
+
metaspace: mockMetaspace,
|
|
967
|
+
space: mockSpace as any,
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
const proofs = signedKeystone.data!.proofs;
|
|
971
|
+
iReckon(sir, proofs.length).asTo('proof count').willEqual(1);
|
|
972
|
+
|
|
973
|
+
const solutions = proofs[0].solutions;
|
|
974
|
+
// 2 Sequential + 2 Random = 4
|
|
975
|
+
iReckon(sir, solutions.length).asTo('solution count').willEqual(4);
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
await ifWeMight(sir, 'verifies the math manually (White-box Crypto Check)', async () => {
|
|
979
|
+
const proof = signedKeystone.data!.proofs[0];
|
|
980
|
+
const poolSnapshot = genesisKeystone.data!.challengePools.find(p => p.id === poolId)!;
|
|
981
|
+
|
|
982
|
+
// We iterate every solution in the proof and MANUALLY verify the hash relationship
|
|
983
|
+
// bypassing the Service's validation logic to ensure the raw math holds up.
|
|
984
|
+
|
|
985
|
+
for (const solution of proof.solutions) {
|
|
986
|
+
// 1. Find the challenge in the *Previous* frame (Genesis)
|
|
987
|
+
const challenge = poolSnapshot.challenges[solution.challengeId];
|
|
988
|
+
|
|
989
|
+
if (!challenge) {
|
|
990
|
+
throw new Error(`Test Failure: Solution references ID ${solution.challengeId} which was not in Genesis pool.`);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// 2. Re-implement HashReveal V1 verification logic locally in the test
|
|
994
|
+
// Hash(Salt + Value + Salt)
|
|
995
|
+
// Note: rounds=1 in standard config
|
|
996
|
+
const indexSalt = solution.challengeId;
|
|
997
|
+
const calculatedHash = await hash({
|
|
998
|
+
s: `${indexSalt}${solution.value}${indexSalt}`,
|
|
999
|
+
algorithm: 'SHA-256'
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
// 3. Assert
|
|
1003
|
+
iReckon(sir, calculatedHash).asTo(`Manual hash verification for ${solution.challengeId}`).willEqual(challenge.hash);
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
await ifWeMight(sir, 'verifies FIFO logic (Deterministic Selection)', async () => {
|
|
1008
|
+
const proof = signedKeystone.data!.proofs[0];
|
|
1009
|
+
const poolSnapshot = genesisKeystone.data!.challengePools.find(p => p.id === poolId)!;
|
|
1010
|
+
|
|
1011
|
+
// The first N keys in the pool should be the FIFO targets.
|
|
1012
|
+
// Assumption: Object.keys returns insertion order (Standard in modern JS engines)
|
|
1013
|
+
const allIds = Object.keys(poolSnapshot.challenges);
|
|
1014
|
+
const expectedFifoIds = allIds.slice(0, 2);
|
|
1015
|
+
|
|
1016
|
+
const solvedIds = proof.solutions.map(s => s.challengeId);
|
|
1017
|
+
|
|
1018
|
+
// Check that our solution list *includes* the expected FIFO IDs
|
|
1019
|
+
const hasFirst = solvedIds.includes(expectedFifoIds[0]);
|
|
1020
|
+
const hasSecond = solvedIds.includes(expectedFifoIds[1]);
|
|
1021
|
+
|
|
1022
|
+
iReckon(sir, hasFirst).asTo(`Solution includes 1st FIFO ID (${expectedFifoIds[0]})`).isGonnaBeTrue();
|
|
1023
|
+
iReckon(sir, hasSecond).asTo(`Solution includes 2nd FIFO ID (${expectedFifoIds[1]})`).isGonnaBeTrue();
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
await ifWeMight(sir, 'verifies Next-Gen Hash-Based Target Binding Selection', async () => {
|
|
1027
|
+
const bindingConfig = createStandardPoolConfig({
|
|
1028
|
+
id: "binding_pool",
|
|
1029
|
+
salt: "binding_salt",
|
|
1030
|
+
sequential: 0,
|
|
1031
|
+
random: 0,
|
|
1032
|
+
targetBinding: 3,
|
|
1033
|
+
size: 20
|
|
1034
|
+
}) as KeystonePoolConfig_HashV1;
|
|
1035
|
+
|
|
1036
|
+
const tempGenesis = await service.genesis({
|
|
1037
|
+
masterSecret: aliceSecret,
|
|
1038
|
+
configs: [bindingConfig],
|
|
1039
|
+
metaspace: mockMetaspace,
|
|
1040
|
+
space: mockSpace as any,
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
// Evolve using the binding pool
|
|
1044
|
+
const evolved = await service.sign({
|
|
1045
|
+
latestKeystone: tempGenesis,
|
|
1046
|
+
masterSecret: aliceSecret,
|
|
1047
|
+
poolId: "binding_pool",
|
|
1048
|
+
claim: {
|
|
1049
|
+
verb: "sign",
|
|
1050
|
+
target: "some_target_address^gib"
|
|
1051
|
+
},
|
|
1052
|
+
metaspace: mockMetaspace,
|
|
1053
|
+
space: mockSpace as any,
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
const proof = evolved.data!.proofs[0];
|
|
1057
|
+
iReckon(sir, proof.solutions.length).asTo('proof solutions count').willEqual(3);
|
|
1058
|
+
|
|
1059
|
+
// Now, let's verify that the selection is perfectly deterministic.
|
|
1060
|
+
// If we run selectChallengeIds manually with the same pool and target, it must return the same IDs.
|
|
1061
|
+
const poolSnapshot = tempGenesis.data!.challengePools.find(p => p.id === "binding_pool")!;
|
|
1062
|
+
const selectedIds = await selectChallengeIds({
|
|
1063
|
+
pool: poolSnapshot,
|
|
1064
|
+
targetAddr: "some_target_address^gib"
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
const proofIds = proof.solutions.map(s => s.challengeId);
|
|
1068
|
+
iReckon(sir, selectedIds.length).asTo('selected IDs count').willEqual(3);
|
|
1069
|
+
for (const id of selectedIds) {
|
|
1070
|
+
iReckon(sir, proofIds.includes(id)).asTo(`proof includes selected ID ${id}`).isGonnaBeTrue();
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
await ifWeMight(sir, 'fails validation if total requested challenges >= size', async () => {
|
|
1075
|
+
const invalidConfig = createStandardPoolConfig({
|
|
1076
|
+
id: "invalid_pool",
|
|
1077
|
+
salt: "invalid_salt",
|
|
1078
|
+
sequential: 5,
|
|
1079
|
+
random: 5,
|
|
1080
|
+
targetBinding: 6,
|
|
1081
|
+
size: 15 // total requested is 5+5+6 = 16 >= 15
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
const errors = await validateChallengePool({ pool: { id: "invalid_pool", config: invalidConfig, challenges: {} } });
|
|
1085
|
+
iReckon(sir, errors.length > 0).asTo('errors length is positive').isGonnaBeTrue();
|
|
1086
|
+
const hasCorrectError = errors.some(e => e.includes("Total requested challenges"));
|
|
1087
|
+
iReckon(sir, hasCorrectError).asTo('validation error content').isGonnaBeTrue();
|
|
1088
|
+
});
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
await respecfully(sir, 'DTO & Serialization', async () => {
|
|
1092
|
+
|
|
1093
|
+
await ifWeMight(sir, 'survives a clone/JSON-cycle without corruption', async () => {
|
|
1094
|
+
// 1. Create a DTO (simulate network transmission/storage)
|
|
1095
|
+
// 'clone' does a JSON stringify/parse under the hood (usually) or structured clone.
|
|
1096
|
+
const dto = clone(signedKeystone);
|
|
1097
|
+
|
|
1098
|
+
// 2. Structural checks
|
|
1099
|
+
iReckon(sir, dto).asTo('dto exists').isGonnaBeTruthy();
|
|
1100
|
+
iReckon(sir, dto.data).asTo('dto data').isGonnaBeTruthy();
|
|
1101
|
+
iReckon(sir, dto.data!.proofs).asTo('dto proofs').isGonnaBeTruthy();
|
|
1102
|
+
|
|
1103
|
+
// 3. Functional check: Can the service validate this DTO?
|
|
1104
|
+
// This ensures no prototypes or hidden properties were lost that the service depends on.
|
|
1105
|
+
const errors = await service.validate({
|
|
1106
|
+
prevIbGib: genesisKeystone,
|
|
1107
|
+
currentIbGib: dto, // Passing the DTO, not the original object
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
iReckon(sir, errors.length).asTo('DTO validation errors').willEqual(0);
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
await ifWeMight(sir, 'ensures data contains no functions or circular refs', async () => {
|
|
1114
|
+
// A crude but effective test: ensure JSON.stringify doesn't throw
|
|
1115
|
+
// and the result is equal to the object (if we parsed it back).
|
|
1116
|
+
|
|
1117
|
+
const jsonStr = JSON.stringify(signedKeystone);
|
|
1118
|
+
const parsed = JSON.parse(jsonStr);
|
|
1119
|
+
|
|
1120
|
+
// Compare specific deep fields
|
|
1121
|
+
const originalSolution = signedKeystone.data!.proofs[0].solutions[0].value;
|
|
1122
|
+
const parsedSolution = parsed.data.proofs[0].solutions[0].value;
|
|
1123
|
+
|
|
1124
|
+
iReckon(sir, parsedSolution).asTo('deep property survives stringify').willEqual(originalSolution);
|
|
1125
|
+
|
|
1126
|
+
// Ensure no extra properties were lost
|
|
1127
|
+
// FIX: JSON.stringify removes keys with 'undefined' values.
|
|
1128
|
+
// We must filter the original keys to match this behavior for a fair comparison.
|
|
1129
|
+
const origKeys = Object.keys(signedKeystone.data!)
|
|
1130
|
+
.filter(k => (signedKeystone.data as any)[k] !== undefined);
|
|
1131
|
+
|
|
1132
|
+
const parsedKeys = Object.keys(parsed.data);
|
|
1133
|
+
iReckon(sir, parsedKeys.length).asTo('key count matches').willEqual(origKeys.length);
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
await respecfully(sir, 'KeystoneProfileBuilder and Modular Schemas', async () => {
|
|
1139
|
+
|
|
1140
|
+
await ifWeMight(sir, 'compiles configs successfully with medium profile', async () => {
|
|
1141
|
+
const builder = KeystoneProfileBuilder.buildKeystone('medium')
|
|
1142
|
+
.withUsername('alice')
|
|
1143
|
+
.withEmail('alice@example.com')
|
|
1144
|
+
.withDescription('Alice test keystone')
|
|
1145
|
+
.withDetails({ extraInfo: 'abc' });
|
|
1146
|
+
|
|
1147
|
+
const configs = await builder.compileConfigs();
|
|
1148
|
+
iReckon(sir, configs.length).asTo('number of pools').willEqual(5);
|
|
1149
|
+
|
|
1150
|
+
// Verify unique salt derivation per pool
|
|
1151
|
+
const salts = configs.map(c => c.salt);
|
|
1152
|
+
const uniqueSalts = new Set(salts);
|
|
1153
|
+
iReckon(sir, uniqueSalts.size).asTo('unique salts count').willEqual(5);
|
|
1154
|
+
|
|
1155
|
+
// Verify frameDetails unification
|
|
1156
|
+
const details = builder.getFrameDetails();
|
|
1157
|
+
iReckon(sir, details.username).asTo('username').willEqual('alice');
|
|
1158
|
+
iReckon(sir, details.email).asTo('email').willEqual('alice@example.com');
|
|
1159
|
+
iReckon(sir, details.description).asTo('description').willEqual('Alice test keystone');
|
|
1160
|
+
iReckon(sir, details.extraInfo).asTo('extraInfo').willEqual('abc');
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
await ifWeMight(sir, 'compiles configs successfully with domain profile (omitting connect)', async () => {
|
|
1164
|
+
const builder = KeystoneProfileBuilder.buildKeystone('domain')
|
|
1165
|
+
.withUsername('alice')
|
|
1166
|
+
.withEmail('alice@example.com')
|
|
1167
|
+
.withDescription('Alice domain keystone');
|
|
1168
|
+
|
|
1169
|
+
const configs = await builder.compileConfigs();
|
|
1170
|
+
iReckon(sir, configs.length).asTo('number of pools').willEqual(4);
|
|
1171
|
+
|
|
1172
|
+
const connectPool = configs.find(c => c.id === 'connect');
|
|
1173
|
+
iReckon(sir, connectPool).asTo('connect pool omitted').isGonnaBeUndefined();
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
await ifWeMight(sir, 'supports customized overrides', async () => {
|
|
1177
|
+
const builder = KeystoneProfileBuilder.buildKeystone('test')
|
|
1178
|
+
.customizePool('sync', {
|
|
1179
|
+
behavior: {
|
|
1180
|
+
size: 15,
|
|
1181
|
+
targetBindingCount: 4
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
const configs = await builder.compileConfigs();
|
|
1186
|
+
const syncPool = configs.find(c => c.id === 'sync')!;
|
|
1187
|
+
iReckon(sir, syncPool.behavior.size).asTo('customized size').willEqual(15);
|
|
1188
|
+
iReckon(sir, syncPool.behavior.targetBindingCount).asTo('customized target binding').willEqual(4);
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
await respecfully(sir, 'Keystone Metadata Validation', async () => {
|
|
1194
|
+
|
|
1195
|
+
await ifWeMight(sir, 'validates correct username and description', async () => {
|
|
1196
|
+
const service = new KeystoneService_V1();
|
|
1197
|
+
const space = new MockIbGibSpace();
|
|
1198
|
+
const metaspace = new MockMetaspaceService(space);
|
|
1199
|
+
|
|
1200
|
+
const builder = KeystoneProfileBuilder.buildKeystone('test')
|
|
1201
|
+
.withUsername('alice_123.test')
|
|
1202
|
+
.withDescription('Valid description: yes, it is!')
|
|
1203
|
+
.withDetails({ client: 'respec' });
|
|
1204
|
+
|
|
1205
|
+
const configs = await builder.compileConfigs();
|
|
1206
|
+
const frameDetails = builder.getFrameDetails();
|
|
1207
|
+
|
|
1208
|
+
// Should succeed without error
|
|
1209
|
+
const keystoneIbGib = await service.genesis({
|
|
1210
|
+
masterSecret: 'mysecret',
|
|
1211
|
+
configs,
|
|
1212
|
+
metaspace: metaspace as any,
|
|
1213
|
+
space: space as any,
|
|
1214
|
+
frameDetails
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
const errors = await service.validateGenesisKeystone({ keystoneIbGib });
|
|
1218
|
+
iReckon(sir, errors.length).asTo('errors count').willEqual(0);
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
await ifWeMight(sir, 'fails validation for invalid username length or character', async () => {
|
|
1222
|
+
const service = new KeystoneService_V1();
|
|
1223
|
+
const space = new MockIbGibSpace();
|
|
1224
|
+
const metaspace = new MockMetaspaceService(space);
|
|
1225
|
+
|
|
1226
|
+
const builder = KeystoneProfileBuilder.buildKeystone('test')
|
|
1227
|
+
.withUsername('invalid_user_name_that_is_way_too_long_for_the_sixty_three_char_limit_and_will_fail_regex_check')
|
|
1228
|
+
.withDetails({ client: 'respec' });
|
|
1229
|
+
|
|
1230
|
+
const configs = await builder.compileConfigs();
|
|
1231
|
+
const frameDetails = builder.getFrameDetails();
|
|
1232
|
+
|
|
1233
|
+
const keystoneIbGib = await service.genesis({
|
|
1234
|
+
masterSecret: 'mysecret',
|
|
1235
|
+
configs,
|
|
1236
|
+
metaspace: metaspace as any,
|
|
1237
|
+
space: space as any,
|
|
1238
|
+
frameDetails
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
const errors = await service.validateGenesisKeystone({ keystoneIbGib });
|
|
1242
|
+
iReckon(sir, errors.length).asTo('errors count').willEqual(1);
|
|
1243
|
+
iReckon(sir, errors[0]).includes('invalid username');
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
await ifWeMight(sir, 'fails validation for invalid description characters', async () => {
|
|
1247
|
+
const service = new KeystoneService_V1();
|
|
1248
|
+
const space = new MockIbGibSpace();
|
|
1249
|
+
const metaspace = new MockMetaspaceService(space);
|
|
1250
|
+
|
|
1251
|
+
const builder = KeystoneProfileBuilder.buildKeystone('test')
|
|
1252
|
+
.withUsername('alice')
|
|
1253
|
+
.withDescription('Invalid desc with weird emoji 🚀')
|
|
1254
|
+
.withDetails({ client: 'respec' });
|
|
1255
|
+
|
|
1256
|
+
const configs = await builder.compileConfigs();
|
|
1257
|
+
const frameDetails = builder.getFrameDetails();
|
|
1258
|
+
|
|
1259
|
+
const keystoneIbGib = await service.genesis({
|
|
1260
|
+
masterSecret: 'mysecret',
|
|
1261
|
+
configs,
|
|
1262
|
+
metaspace: metaspace as any,
|
|
1263
|
+
space: space as any,
|
|
1264
|
+
frameDetails
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
const errors = await service.validateGenesisKeystone({ keystoneIbGib });
|
|
1268
|
+
iReckon(sir, errors.length).asTo('errors count').willEqual(1);
|
|
1269
|
+
iReckon(sir, errors[0]).includes('invalid description');
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
});
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
// ===========================================================================
|
|
1277
|
+
// SUITE E: DELEGATION (Register/Unregister)
|
|
1278
|
+
// ===========================================================================
|
|
1279
|
+
|
|
1280
|
+
await respecfully(sir, 'Suite E: Delegation (Register/Unregister)', async () => {
|
|
1281
|
+
|
|
1282
|
+
const service = new KeystoneService_V1();
|
|
1283
|
+
const parentSecret = "ParentMasterSecret_123";
|
|
1284
|
+
const delegateSecret = "DelegateMasterSecret_456";
|
|
1285
|
+
|
|
1286
|
+
let mockSpace: MockIbGibSpace;
|
|
1287
|
+
let mockMetaspace: any;
|
|
1288
|
+
|
|
1289
|
+
let parentKeystone: KeystoneIbGib_V1;
|
|
1290
|
+
let delegateKeystone: KeystoneIbGib_V1;
|
|
1291
|
+
|
|
1292
|
+
firstOfEach(sir, async () => {
|
|
1293
|
+
mockSpace = new MockIbGibSpace();
|
|
1294
|
+
mockMetaspace = new MockMetaspaceService(mockSpace);
|
|
1295
|
+
|
|
1296
|
+
const salt = (await getUUID()).substring(0, 16);
|
|
1297
|
+
const defaultConfig = createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt });
|
|
1298
|
+
const manageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: salt + "_manage" });
|
|
1299
|
+
manageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE];
|
|
1300
|
+
|
|
1301
|
+
// 1. Create Parent Keystone (with manage pool)
|
|
1302
|
+
parentKeystone = await service.genesis({
|
|
1303
|
+
masterSecret: parentSecret,
|
|
1304
|
+
configs: [defaultConfig, manageConfig],
|
|
1305
|
+
metaspace: mockMetaspace,
|
|
1306
|
+
space: mockSpace as any,
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
// 2. Create Delegate Keystone
|
|
1310
|
+
const delegateConfig = createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt: salt + "_delegate" });
|
|
1311
|
+
delegateKeystone = await service.genesis({
|
|
1312
|
+
masterSecret: delegateSecret,
|
|
1313
|
+
configs: [delegateConfig],
|
|
1314
|
+
metaspace: mockMetaspace,
|
|
1315
|
+
space: mockSpace as any,
|
|
1316
|
+
});
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
await ifWeMight(sir, 'registers a delegate keystone and evolves parent using manage pool', async () => {
|
|
1320
|
+
// Register the delegate
|
|
1321
|
+
const evolvedParent = await service.registerDelegate({
|
|
1322
|
+
parentKeystone,
|
|
1323
|
+
delegateKeystone,
|
|
1324
|
+
masterSecret: parentSecret,
|
|
1325
|
+
metaspace: mockMetaspace,
|
|
1326
|
+
space: mockSpace as any,
|
|
1327
|
+
allowedVerbs: [KEYSTONE_VERB_SYNC],
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
iReckon(sir, evolvedParent).asTo('evolved parent exists').isGonnaBeTruthy();
|
|
1331
|
+
iReckon(sir, evolvedParent.data?.delegates).asTo('delegates map exists').isGonnaBeTruthy();
|
|
1332
|
+
|
|
1333
|
+
const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone });
|
|
1334
|
+
if (!delegateTjpAddr) { throw new Error(`(UNEXPECTED) delegateTjpAddr falsy? (E: 312e986ce4b2ed6c084c03e8f45fa826)`); }
|
|
1335
|
+
const delegateInfo = evolvedParent.data?.delegates?.[delegateTjpAddr];
|
|
1336
|
+
iReckon(sir, delegateInfo).asTo('delegate info registered').isGonnaBeTruthy();
|
|
1337
|
+
iReckon(sir, delegateInfo?.delegateTjpAddr).asTo('delegate tjp matches').willEqual(delegateTjpAddr);
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
await ifWeMight(sir, 'gets correct delegate status via parent TJP address', async () => {
|
|
1341
|
+
const parentTjpAddr = getTjpAddr({ ibGib: parentKeystone });
|
|
1342
|
+
if (!parentTjpAddr) { throw new Error(`(UNEXPECTED) parentTjpAddr falsy? (E: b7ce2835ccd951d8c99257328be3b826)`); }
|
|
1343
|
+
const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone });
|
|
1344
|
+
if (!delegateTjpAddr) { throw new Error(`(UNEXPECTED) delegateTjpAddr falsy? (E: 3e4878ea19dd8034e827466a8bf09526)`); }
|
|
1345
|
+
|
|
1346
|
+
// Before registration
|
|
1347
|
+
const statusBefore = await service.getDelegateStatus({
|
|
1348
|
+
parentTjpAddr: parentTjpAddr,
|
|
1349
|
+
delegateTjpAddr: delegateTjpAddr,
|
|
1350
|
+
metaspace: mockMetaspace,
|
|
1351
|
+
space: mockSpace as any,
|
|
1352
|
+
});
|
|
1353
|
+
iReckon(sir, statusBefore.registered).asTo('initially unregistered').isGonnaBeFalse();
|
|
1354
|
+
|
|
1355
|
+
// Register
|
|
1356
|
+
const evolvedParent = await service.registerDelegate({
|
|
1357
|
+
parentKeystone,
|
|
1358
|
+
delegateKeystone,
|
|
1359
|
+
masterSecret: parentSecret,
|
|
1360
|
+
metaspace: mockMetaspace,
|
|
1361
|
+
space: mockSpace as any,
|
|
1362
|
+
allowedVerbs: [KEYSTONE_VERB_SYNC],
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
// After registration
|
|
1366
|
+
const statusAfter = await service.getDelegateStatus({
|
|
1367
|
+
parentTjpAddr: parentTjpAddr,
|
|
1368
|
+
delegateTjpAddr: delegateTjpAddr,
|
|
1369
|
+
metaspace: mockMetaspace,
|
|
1370
|
+
space: mockSpace as any,
|
|
1371
|
+
});
|
|
1372
|
+
iReckon(sir, statusAfter.registered).asTo('registered status').isGonnaBeTrue();
|
|
1373
|
+
iReckon(sir, statusAfter.delegateInfo?.delegateTjpAddr).asTo('status tjp matches').willEqual(delegateTjpAddr);
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
await ifWeMight(sir, 'unregisters a registered delegate keystone', async () => {
|
|
1377
|
+
// Register first
|
|
1378
|
+
const evolvedParent = await service.registerDelegate({
|
|
1379
|
+
parentKeystone,
|
|
1380
|
+
delegateKeystone,
|
|
1381
|
+
masterSecret: parentSecret,
|
|
1382
|
+
metaspace: mockMetaspace,
|
|
1383
|
+
space: mockSpace as any,
|
|
1384
|
+
allowedVerbs: [KEYSTONE_VERB_SYNC],
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone });
|
|
1388
|
+
if (!delegateTjpAddr) { throw new Error(`(UNEXPECTED) delegateTjpAddr falsy? (E: 486b486a8c984a52b8d2b22418c20d26)`); }
|
|
1389
|
+
|
|
1390
|
+
// Unregister
|
|
1391
|
+
const finalParent = await service.unregisterDelegate({
|
|
1392
|
+
parentKeystone: evolvedParent,
|
|
1393
|
+
delegateTjpAddr,
|
|
1394
|
+
masterSecret: parentSecret,
|
|
1395
|
+
metaspace: mockMetaspace,
|
|
1396
|
+
space: mockSpace as any,
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
iReckon(sir, finalParent).asTo('final evolved parent exists').isGonnaBeTruthy();
|
|
1400
|
+
const info = finalParent.data?.delegates?.[delegateTjpAddr];
|
|
1401
|
+
iReckon(sir, info).asTo('delegate info removed').isGonnaBeUndefined();
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
await ifWeMight(sir, 'throws if parent does not have a manage pool', async () => {
|
|
1405
|
+
// Create a parent WITHOUT a manage pool
|
|
1406
|
+
const salt = (await getUUID()).substring(0, 16);
|
|
1407
|
+
const defaultConfig = createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt });
|
|
1408
|
+
const restrictedParent = await service.genesis({
|
|
1409
|
+
masterSecret: parentSecret,
|
|
1410
|
+
configs: [defaultConfig],
|
|
1411
|
+
metaspace: mockMetaspace,
|
|
1412
|
+
space: mockSpace as any,
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
let errorCaught = false;
|
|
1416
|
+
try {
|
|
1417
|
+
await service.registerDelegate({
|
|
1418
|
+
parentKeystone: restrictedParent,
|
|
1419
|
+
delegateKeystone,
|
|
1420
|
+
masterSecret: parentSecret,
|
|
1421
|
+
metaspace: mockMetaspace,
|
|
1422
|
+
space: mockSpace as any,
|
|
1423
|
+
allowedVerbs: [KEYSTONE_VERB_SYNC],
|
|
1424
|
+
});
|
|
1425
|
+
} catch (e: any) {
|
|
1426
|
+
errorCaught = true;
|
|
1427
|
+
iReckon(sir, e.message).includes('Manage pool is required');
|
|
1428
|
+
}
|
|
1429
|
+
iReckon(sir, errorCaught).asTo('delegation rejected').isGonnaBeTrue();
|
|
1430
|
+
});
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
await respecfully(sir, 'Suite G: Delegate & IB Validation', async () => {
|
|
1434
|
+
const service = new KeystoneService_V1();
|
|
1435
|
+
const parentSecret = "ParentMasterSecret_SuiteG";
|
|
1436
|
+
const delegateSecret = "DelegateMasterSecret_SuiteG";
|
|
1437
|
+
|
|
1438
|
+
let mockSpace: MockIbGibSpace;
|
|
1439
|
+
let mockMetaspace: any;
|
|
1440
|
+
|
|
1441
|
+
let parentKeystone: KeystoneIbGib_V1;
|
|
1442
|
+
let delegateKeystone: KeystoneIbGib_V1;
|
|
1443
|
+
|
|
1444
|
+
firstOfEach(sir, async () => {
|
|
1445
|
+
mockSpace = new MockIbGibSpace();
|
|
1446
|
+
mockMetaspace = new MockMetaspaceService(mockSpace);
|
|
1447
|
+
|
|
1448
|
+
const salt1 = (await getUUID()).substring(0, 16);
|
|
1449
|
+
const salt2 = (await getUUID()).substring(0, 16);
|
|
1450
|
+
|
|
1451
|
+
const manageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: salt2 });
|
|
1452
|
+
manageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE];
|
|
1453
|
+
|
|
1454
|
+
const configs = [
|
|
1455
|
+
createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt: salt1 }),
|
|
1456
|
+
manageConfig
|
|
1457
|
+
];
|
|
1458
|
+
|
|
1459
|
+
parentKeystone = await service.genesis({
|
|
1460
|
+
masterSecret: parentSecret,
|
|
1461
|
+
configs,
|
|
1462
|
+
metaspace: mockMetaspace,
|
|
1463
|
+
space: mockSpace as any,
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
delegateKeystone = await service.genesis({
|
|
1467
|
+
masterSecret: delegateSecret,
|
|
1468
|
+
configs: [createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt: salt1 })],
|
|
1469
|
+
metaspace: mockMetaspace,
|
|
1470
|
+
space: mockSpace as any,
|
|
1471
|
+
});
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
await ifWeMight(sir, 'validateKeystoneIb returns true for valid keystone ib and false for others', async () => {
|
|
1475
|
+
iReckon(sir, await validateKeystoneIb({ ib: 'keystone' })).isGonnaBeTrue();
|
|
1476
|
+
iReckon(sir, await validateKeystoneIb({ ib: 'keystone user info' })).isGonnaBeTrue();
|
|
1477
|
+
iReckon(sir, await validateKeystoneIb({ ib: 'invalidAtom' })).isGonnaBeFalse();
|
|
1478
|
+
iReckon(sir, await validateKeystoneIb({ ib: 'not_keystone sub_atom' })).isGonnaBeFalse();
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
await ifWeMight(sir, 'validateGenesisKeystone fails validation if ib is invalid', async () => {
|
|
1482
|
+
const invalidGenesis = {
|
|
1483
|
+
...parentKeystone,
|
|
1484
|
+
ib: 'not_a_keystone^' + parentKeystone.gib,
|
|
1485
|
+
};
|
|
1486
|
+
const errors = await validateGenesisKeystone({ keystoneIbGib: invalidGenesis });
|
|
1487
|
+
iReckon(sir, errors.length > 0).isGonnaBeTrue();
|
|
1488
|
+
iReckon(sir, errors[0]).includes('invalid keystone ib');
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
await ifWeMight(sir, 'validateKeystoneTransition passes for valid delegate registration', async () => {
|
|
1492
|
+
const evolvedParent = await service.registerDelegate({
|
|
1493
|
+
parentKeystone,
|
|
1494
|
+
delegateKeystone,
|
|
1495
|
+
masterSecret: parentSecret,
|
|
1496
|
+
metaspace: mockMetaspace,
|
|
1497
|
+
space: mockSpace as any,
|
|
1498
|
+
allowedVerbs: [KEYSTONE_VERB_SYNC],
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
const errors = await validateKeystoneTransition({
|
|
1502
|
+
currentIbGib: evolvedParent,
|
|
1503
|
+
prevIbGib: parentKeystone,
|
|
1504
|
+
});
|
|
1505
|
+
iReckon(sir, errors.length).willEqual(0);
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
await ifWeMight(sir, 'validateKeystoneTransition fails if delegates map is mutated without manage proof', async () => {
|
|
1509
|
+
const evolvedParent = await service.registerDelegate({
|
|
1510
|
+
parentKeystone,
|
|
1511
|
+
delegateKeystone,
|
|
1512
|
+
masterSecret: parentSecret,
|
|
1513
|
+
metaspace: mockMetaspace,
|
|
1514
|
+
space: mockSpace as any,
|
|
1515
|
+
allowedVerbs: [KEYSTONE_VERB_SYNC],
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
const tamperedParent = JSON.parse(JSON.stringify(evolvedParent)) as KeystoneIbGib_V1;
|
|
1519
|
+
|
|
1520
|
+
// Remove the manage verb proof by converting it to sign
|
|
1521
|
+
for (const proof of tamperedParent.data.proofs) {
|
|
1522
|
+
if (proof.claim) {
|
|
1523
|
+
proof.claim.verb = 'sign';
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
const errors = await validateKeystoneTransition({
|
|
1528
|
+
currentIbGib: tamperedParent,
|
|
1529
|
+
prevIbGib: parentKeystone,
|
|
1530
|
+
});
|
|
1531
|
+
iReckon(sir, errors.length > 0).isGonnaBeTrue();
|
|
1532
|
+
iReckon(sir, errors.some(e => e.includes('Policy Violation: Delegates map was mutated'))).isGonnaBeTrue();
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1535
|
+
await ifWeMight(sir, 'validateKeystoneTransition fails if delegate key does not match delegateTjpAddr', async () => {
|
|
1536
|
+
const evolvedParent = await service.registerDelegate({
|
|
1537
|
+
parentKeystone,
|
|
1538
|
+
delegateKeystone,
|
|
1539
|
+
masterSecret: parentSecret,
|
|
1540
|
+
metaspace: mockMetaspace,
|
|
1541
|
+
space: mockSpace as any,
|
|
1542
|
+
allowedVerbs: [KEYSTONE_VERB_SYNC],
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
const tamperedParent = JSON.parse(JSON.stringify(evolvedParent)) as KeystoneIbGib_V1;
|
|
1546
|
+
const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone });
|
|
1547
|
+
if (!delegateTjpAddr) { throw new Error(`delegateTjpAddr falsy?`); }
|
|
1548
|
+
|
|
1549
|
+
const info = tamperedParent.data.delegates![delegateTjpAddr];
|
|
1550
|
+
delete tamperedParent.data.delegates![delegateTjpAddr];
|
|
1551
|
+
tamperedParent.data.delegates!['mismatched_key'] = info;
|
|
1552
|
+
|
|
1553
|
+
const errors = await validateKeystoneTransition({
|
|
1554
|
+
currentIbGib: tamperedParent,
|
|
1555
|
+
prevIbGib: parentKeystone,
|
|
1556
|
+
});
|
|
1557
|
+
iReckon(sir, errors.length > 0).isGonnaBeTrue();
|
|
1558
|
+
iReckon(sir, errors.some(e => e.includes('does not match delegateTjpAddr'))).isGonnaBeTrue();
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1561
|
+
await ifWeMight(sir, 'validateKeystoneTransition fails if delegate addresses have invalid keystone ib', async () => {
|
|
1562
|
+
const evolvedParent = await service.registerDelegate({
|
|
1563
|
+
parentKeystone,
|
|
1564
|
+
delegateKeystone,
|
|
1565
|
+
masterSecret: parentSecret,
|
|
1566
|
+
metaspace: mockMetaspace,
|
|
1567
|
+
space: mockSpace as any,
|
|
1568
|
+
allowedVerbs: [KEYSTONE_VERB_SYNC],
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
const tamperedParent = JSON.parse(JSON.stringify(evolvedParent)) as KeystoneIbGib_V1;
|
|
1572
|
+
const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone });
|
|
1573
|
+
if (!delegateTjpAddr) { throw new Error(`delegateTjpAddr falsy?`); }
|
|
1574
|
+
|
|
1575
|
+
tamperedParent.data.delegates![delegateTjpAddr].delegateAddr = 'invalid_ib^12345';
|
|
1576
|
+
const errors = await validateKeystoneTransition({
|
|
1577
|
+
currentIbGib: tamperedParent,
|
|
1578
|
+
prevIbGib: parentKeystone,
|
|
1579
|
+
});
|
|
1580
|
+
iReckon(sir, errors.length > 0).isGonnaBeTrue();
|
|
1581
|
+
iReckon(sir, errors.some(e => e.includes('not a valid keystone address'))).isGonnaBeTrue();
|
|
1582
|
+
});
|
|
1583
|
+
|
|
1584
|
+
await ifWeMight(sir, 'validateKeystoneTransition fails if thisAddr is not in the parent timeline history', async () => {
|
|
1585
|
+
const evolvedParent = await service.registerDelegate({
|
|
1586
|
+
parentKeystone,
|
|
1587
|
+
delegateKeystone,
|
|
1588
|
+
masterSecret: parentSecret,
|
|
1589
|
+
metaspace: mockMetaspace,
|
|
1590
|
+
space: mockSpace as any,
|
|
1591
|
+
allowedVerbs: [KEYSTONE_VERB_SYNC],
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
const tamperedParent = JSON.parse(JSON.stringify(evolvedParent)) as KeystoneIbGib_V1;
|
|
1595
|
+
const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone });
|
|
1596
|
+
if (!delegateTjpAddr) { throw new Error(`delegateTjpAddr falsy?`); }
|
|
1597
|
+
|
|
1598
|
+
tamperedParent.data.delegates![delegateTjpAddr].thisAddr = 'keystone^randomgib';
|
|
1599
|
+
|
|
1600
|
+
const errors = await validateKeystoneTransition({
|
|
1601
|
+
currentIbGib: tamperedParent,
|
|
1602
|
+
prevIbGib: parentKeystone,
|
|
1603
|
+
});
|
|
1604
|
+
iReckon(sir, errors.length > 0).isGonnaBeTrue();
|
|
1605
|
+
iReckon(sir, errors.some(e => e.includes('not a valid historical address in the parent keystone timeline'))).isGonnaBeTrue();
|
|
1606
|
+
});
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
// ===========================================================================
|
|
1610
|
+
// SUITE G: COMBINED POOLS & PASSWORD ROTATION
|
|
1611
|
+
// ===========================================================================
|
|
1612
|
+
|
|
1613
|
+
await respecfully(sir, 'Suite G: Combined Pools & Password Rotation', async () => {
|
|
1614
|
+
|
|
1615
|
+
const service = new KeystoneService_V1();
|
|
1616
|
+
const userSecret = "UserMasterSecret_123";
|
|
1617
|
+
const custodianSecret = "CustodianSecret_456";
|
|
1618
|
+
|
|
1619
|
+
let mockSpace: MockIbGibSpace;
|
|
1620
|
+
let mockMetaspace: any;
|
|
1621
|
+
|
|
1622
|
+
firstOfEach(sir, async () => {
|
|
1623
|
+
mockSpace = new MockIbGibSpace();
|
|
1624
|
+
mockMetaspace = new MockMetaspaceService(mockSpace);
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
await ifWeMight(sir, 'revokes a keystone using a separate revocation pool', async () => {
|
|
1628
|
+
const stdSalt = "std_salt";
|
|
1629
|
+
const revokeSalt = "revoke_salt";
|
|
1630
|
+
const stdConfig = createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt: stdSalt });
|
|
1631
|
+
const revokeConfig = createStandardPoolConfig({ id: POOL_ID_REVOKE, salt: revokeSalt });
|
|
1632
|
+
revokeConfig.allowedVerbs = [KEYSTONE_VERB_REVOKE];
|
|
1633
|
+
revokeConfig.behavior.replenish = KeystoneReplenishStrategy.deleteAll;
|
|
1634
|
+
|
|
1635
|
+
const keystone = await service.genesis({
|
|
1636
|
+
masterSecret: userSecret,
|
|
1637
|
+
configs: [stdConfig, revokeConfig],
|
|
1638
|
+
metaspace: mockMetaspace,
|
|
1639
|
+
space: mockSpace as any,
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
const revoked = await service.revoke({
|
|
1643
|
+
latestKeystone: keystone,
|
|
1644
|
+
masterSecret: userSecret,
|
|
1645
|
+
metaspace: mockMetaspace,
|
|
1646
|
+
space: mockSpace as any,
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
iReckon(sir, revoked.data?.revocationInfo).asTo('revocationInfo present').isGonnaBeTruthy();
|
|
1650
|
+
const revokePool = revoked.data?.challengePools.find(p => p.id === POOL_ID_REVOKE)!;
|
|
1651
|
+
iReckon(sir, Object.keys(revokePool.challenges).length).asTo('revoke pool depleted').willEqual(0);
|
|
1652
|
+
});
|
|
1653
|
+
|
|
1654
|
+
await ifWeMight(sir, 'revokes a keystone using a combined manage/revoke pool and empties it', async () => {
|
|
1655
|
+
const salt = "combined_salt";
|
|
1656
|
+
const manageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt });
|
|
1657
|
+
manageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
1658
|
+
|
|
1659
|
+
const keystone = await service.genesis({
|
|
1660
|
+
masterSecret: userSecret,
|
|
1661
|
+
configs: [manageConfig],
|
|
1662
|
+
metaspace: mockMetaspace,
|
|
1663
|
+
space: mockSpace as any,
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
const revoked = await service.revoke({
|
|
1667
|
+
latestKeystone: keystone,
|
|
1668
|
+
masterSecret: userSecret,
|
|
1669
|
+
metaspace: mockMetaspace,
|
|
1670
|
+
space: mockSpace as any,
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
iReckon(sir, revoked.data?.revocationInfo).asTo('revocationInfo present').isGonnaBeTruthy();
|
|
1674
|
+
const managePool = revoked.data?.challengePools.find(p => p.id === POOL_ID_MANAGE)!;
|
|
1675
|
+
iReckon(sir, Object.keys(managePool.challenges).length).asTo('combined manage pool depleted').willEqual(0);
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
await ifWeMight(sir, 'rotates user password successfully when signed by the user manage pool', async () => {
|
|
1679
|
+
const salt = "manage_salt";
|
|
1680
|
+
const manageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt });
|
|
1681
|
+
manageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
1682
|
+
|
|
1683
|
+
const keystone = await service.genesis({
|
|
1684
|
+
masterSecret: userSecret,
|
|
1685
|
+
configs: [manageConfig],
|
|
1686
|
+
metaspace: mockMetaspace,
|
|
1687
|
+
space: mockSpace as any,
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
const newSecret = "NewUserSecret_999";
|
|
1691
|
+
const rotated = await service.changePassword({
|
|
1692
|
+
latestKeystone: keystone,
|
|
1693
|
+
newMasterSecret: newSecret,
|
|
1694
|
+
signingSecret: userSecret,
|
|
1695
|
+
signingPoolId: POOL_ID_MANAGE,
|
|
1696
|
+
metaspace: mockMetaspace,
|
|
1697
|
+
space: mockSpace as any,
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1700
|
+
iReckon(sir, rotated).isGonnaBeTruthy();
|
|
1701
|
+
|
|
1702
|
+
// 1. Try to sign using the new secret (should succeed)
|
|
1703
|
+
const claim = { verb: KEYSTONE_VERB_MANAGE, target: "test_target" };
|
|
1704
|
+
const signedNew = await service.sign({
|
|
1705
|
+
latestKeystone: rotated,
|
|
1706
|
+
masterSecret: newSecret,
|
|
1707
|
+
claim,
|
|
1708
|
+
poolId: POOL_ID_MANAGE,
|
|
1709
|
+
metaspace: mockMetaspace,
|
|
1710
|
+
space: mockSpace as any,
|
|
1711
|
+
});
|
|
1712
|
+
iReckon(sir, signedNew).isGonnaBeTruthy();
|
|
1713
|
+
|
|
1714
|
+
// 2. Try to sign using the old secret (should fail)
|
|
1715
|
+
let errorCaught = false;
|
|
1716
|
+
try {
|
|
1717
|
+
await service.sign({
|
|
1718
|
+
latestKeystone: rotated,
|
|
1719
|
+
masterSecret: userSecret,
|
|
1720
|
+
claim,
|
|
1721
|
+
poolId: POOL_ID_MANAGE,
|
|
1722
|
+
metaspace: mockMetaspace,
|
|
1723
|
+
space: mockSpace as any,
|
|
1724
|
+
});
|
|
1725
|
+
} catch {
|
|
1726
|
+
errorCaught = true;
|
|
1727
|
+
}
|
|
1728
|
+
iReckon(sir, errorCaught).asTo('old secret signing rejected').isGonnaBeTrue();
|
|
1729
|
+
});
|
|
1730
|
+
|
|
1731
|
+
await ifWeMight(sir, 'recovers user password successfully when signed by the custodian manage pool', async () => {
|
|
1732
|
+
const userSalt = "user_salt";
|
|
1733
|
+
const custodianSalt = "custodian_salt";
|
|
1734
|
+
|
|
1735
|
+
const userManageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: userSalt });
|
|
1736
|
+
userManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
1737
|
+
|
|
1738
|
+
const custodianManageConfig = createStandardPoolConfig({ id: POOL_ID_CUSTODIAN_MANAGE, salt: custodianSalt });
|
|
1739
|
+
custodianManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
1740
|
+
|
|
1741
|
+
// We simulate constructing the custodian pool "offline" by the server
|
|
1742
|
+
const strategy = KeystoneStrategyFactory.create({ config: custodianManageConfig });
|
|
1743
|
+
const poolSecret = await strategy.derivePoolSecret({ masterSecret: custodianSecret });
|
|
1744
|
+
const challenges: { [id: string]: any } = {};
|
|
1745
|
+
for (let i = 0; i < custodianManageConfig.behavior.size; i++) {
|
|
1746
|
+
const raw = await hash({ s: `${custodianManageConfig.salt}${Date.now()}${i}` });
|
|
1747
|
+
const challengeId = raw.substring(0, 16);
|
|
1748
|
+
const solution = await strategy.generateSolution({ poolSecret, poolId: custodianManageConfig.id, challengeId });
|
|
1749
|
+
challenges[challengeId] = await strategy.generateChallenge({ solution });
|
|
1750
|
+
}
|
|
1751
|
+
const custodianPool: KeystoneChallengePool = {
|
|
1752
|
+
id: POOL_ID_CUSTODIAN_MANAGE,
|
|
1753
|
+
config: custodianManageConfig,
|
|
1754
|
+
challenges,
|
|
1755
|
+
isForeign: true
|
|
1756
|
+
};
|
|
1757
|
+
|
|
1758
|
+
const keystone = await service.genesis({
|
|
1759
|
+
masterSecret: userSecret,
|
|
1760
|
+
configs: [userManageConfig],
|
|
1761
|
+
metaspace: mockMetaspace,
|
|
1762
|
+
space: mockSpace as any,
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
// Add the custodian pool to the keystone
|
|
1766
|
+
const keystoneWithCustodian = await service.addPools({
|
|
1767
|
+
latestKeystone: keystone,
|
|
1768
|
+
masterSecret: userSecret,
|
|
1769
|
+
newPools: [custodianPool],
|
|
1770
|
+
metaspace: mockMetaspace,
|
|
1771
|
+
space: mockSpace as any,
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
// The user forgets their secret. Recover using custodianSecret!
|
|
1775
|
+
const recoveredSecret = "RecoveredUserSecret_777";
|
|
1776
|
+
const recoveredKeystone = await service.changePassword({
|
|
1777
|
+
latestKeystone: keystoneWithCustodian,
|
|
1778
|
+
newMasterSecret: recoveredSecret,
|
|
1779
|
+
signingSecret: custodianSecret,
|
|
1780
|
+
signingPoolId: POOL_ID_CUSTODIAN_MANAGE,
|
|
1781
|
+
metaspace: mockMetaspace,
|
|
1782
|
+
space: mockSpace as any,
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
iReckon(sir, recoveredKeystone).isGonnaBeTruthy();
|
|
1786
|
+
|
|
1787
|
+
// Verify we can sign with recovered secret
|
|
1788
|
+
const claim = { verb: KEYSTONE_VERB_MANAGE, target: "test_target" };
|
|
1789
|
+
const signedNew = await service.sign({
|
|
1790
|
+
latestKeystone: recoveredKeystone,
|
|
1791
|
+
masterSecret: recoveredSecret,
|
|
1792
|
+
claim,
|
|
1793
|
+
poolId: POOL_ID_MANAGE,
|
|
1794
|
+
metaspace: mockMetaspace,
|
|
1795
|
+
space: mockSpace as any,
|
|
1796
|
+
});
|
|
1797
|
+
iReckon(sir, signedNew).isGonnaBeTruthy();
|
|
1798
|
+
});
|
|
1799
|
+
|
|
1800
|
+
await ifWeMight(sir, 'rotates custodian manage pool successfully', async () => {
|
|
1801
|
+
const userSalt = "user_salt";
|
|
1802
|
+
const custodianSalt = "custodian_salt";
|
|
1803
|
+
|
|
1804
|
+
const userManageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: userSalt });
|
|
1805
|
+
userManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
1806
|
+
|
|
1807
|
+
const custodianManageConfig = createStandardPoolConfig({ id: POOL_ID_CUSTODIAN_MANAGE, salt: custodianSalt });
|
|
1808
|
+
custodianManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
1809
|
+
|
|
1810
|
+
// Custodian Pool genesis
|
|
1811
|
+
const strategy = KeystoneStrategyFactory.create({ config: custodianManageConfig });
|
|
1812
|
+
const poolSecret = await strategy.derivePoolSecret({ masterSecret: custodianSecret });
|
|
1813
|
+
const challenges: { [id: string]: any } = {};
|
|
1814
|
+
for (let i = 0; i < custodianManageConfig.behavior.size; i++) {
|
|
1815
|
+
const raw = await hash({ s: `${custodianManageConfig.salt}${Date.now()}${i}` });
|
|
1816
|
+
const challengeId = raw.substring(0, 16);
|
|
1817
|
+
const solution = await strategy.generateSolution({ poolSecret, poolId: custodianManageConfig.id, challengeId });
|
|
1818
|
+
challenges[challengeId] = await strategy.generateChallenge({ solution });
|
|
1819
|
+
}
|
|
1820
|
+
const custodianPool: KeystoneChallengePool = {
|
|
1821
|
+
id: POOL_ID_CUSTODIAN_MANAGE,
|
|
1822
|
+
config: custodianManageConfig,
|
|
1823
|
+
challenges,
|
|
1824
|
+
isForeign: true
|
|
1825
|
+
};
|
|
1826
|
+
|
|
1827
|
+
const keystone = await service.genesis({
|
|
1828
|
+
masterSecret: userSecret,
|
|
1829
|
+
configs: [userManageConfig],
|
|
1830
|
+
metaspace: mockMetaspace,
|
|
1831
|
+
space: mockSpace as any,
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
const keystoneWithCustodian = await service.addPools({
|
|
1835
|
+
latestKeystone: keystone,
|
|
1836
|
+
masterSecret: userSecret,
|
|
1837
|
+
newPools: [custodianPool],
|
|
1838
|
+
metaspace: mockMetaspace,
|
|
1839
|
+
space: mockSpace as any,
|
|
1840
|
+
});
|
|
1841
|
+
|
|
1842
|
+
// Rotate custodian credentials to new secret
|
|
1843
|
+
const newCustodianSecret = "NewCustodianSecret_888";
|
|
1844
|
+
const newCustodianConfig = createStandardPoolConfig({ id: POOL_ID_CUSTODIAN_MANAGE, salt: "new_custodian_salt" });
|
|
1845
|
+
newCustodianConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
1846
|
+
|
|
1847
|
+
const newStrategy = KeystoneStrategyFactory.create({ config: newCustodianConfig });
|
|
1848
|
+
const newPoolSecret = await newStrategy.derivePoolSecret({ masterSecret: newCustodianSecret });
|
|
1849
|
+
const newChallenges: { [id: string]: any } = {};
|
|
1850
|
+
for (let i = 0; i < newCustodianConfig.behavior.size; i++) {
|
|
1851
|
+
const raw = await hash({ s: `${newCustodianConfig.salt}${Date.now()}${i}` });
|
|
1852
|
+
const challengeId = raw.substring(0, 16);
|
|
1853
|
+
const solution = await newStrategy.generateSolution({ poolSecret: newPoolSecret, poolId: newCustodianConfig.id, challengeId });
|
|
1854
|
+
newChallenges[challengeId] = await newStrategy.generateChallenge({ solution });
|
|
1855
|
+
}
|
|
1856
|
+
const newCustodianPool: KeystoneChallengePool = {
|
|
1857
|
+
id: POOL_ID_CUSTODIAN_MANAGE,
|
|
1858
|
+
config: newCustodianConfig,
|
|
1859
|
+
challenges: newChallenges,
|
|
1860
|
+
isForeign: true
|
|
1861
|
+
};
|
|
1862
|
+
|
|
1863
|
+
// Rotate signed by user manage pool
|
|
1864
|
+
const rotated = await service.replacePool({
|
|
1865
|
+
latestKeystone: keystoneWithCustodian,
|
|
1866
|
+
newPool: newCustodianPool,
|
|
1867
|
+
signingSecret: userSecret,
|
|
1868
|
+
signingPoolId: POOL_ID_MANAGE,
|
|
1869
|
+
metaspace: mockMetaspace,
|
|
1870
|
+
space: mockSpace as any,
|
|
1871
|
+
});
|
|
1872
|
+
|
|
1873
|
+
iReckon(sir, rotated).isGonnaBeTruthy();
|
|
1874
|
+
|
|
1875
|
+
// Verify we can sign with new custodian secret
|
|
1876
|
+
const claim = { verb: KEYSTONE_VERB_MANAGE, target: "test_target" };
|
|
1877
|
+
const signedNew = await service.sign({
|
|
1878
|
+
latestKeystone: rotated,
|
|
1879
|
+
masterSecret: newCustodianSecret,
|
|
1880
|
+
claim,
|
|
1881
|
+
poolId: POOL_ID_CUSTODIAN_MANAGE,
|
|
625
1882
|
metaspace: mockMetaspace,
|
|
626
1883
|
space: mockSpace as any,
|
|
627
1884
|
});
|
|
1885
|
+
iReckon(sir, signedNew).isGonnaBeTruthy();
|
|
628
1886
|
});
|
|
629
1887
|
|
|
630
|
-
await
|
|
631
|
-
|
|
632
|
-
|
|
1888
|
+
await ifWeMight(sir, 'rotates all non-foreign pools during changePassword by default', async () => {
|
|
1889
|
+
const userSalt = "user_salt";
|
|
1890
|
+
const custodianSalt = "custodian_salt";
|
|
1891
|
+
const secondarySalt = "secondary_salt";
|
|
633
1892
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
masterSecret: aliceSecret,
|
|
637
|
-
newPools: [bobPool],
|
|
638
|
-
metaspace: mockMetaspace,
|
|
639
|
-
space: mockSpace as any,
|
|
640
|
-
});
|
|
1893
|
+
const userManageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: userSalt });
|
|
1894
|
+
userManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
641
1895
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
iReckon(sir, pools.length).asTo('pool count increased').willEqual(2);
|
|
646
|
-
|
|
647
|
-
const foundBob = pools.find(p => p.id === "pool_bob");
|
|
648
|
-
iReckon(sir, foundBob).asTo('bob pool exists').isGonnaBeTruthy();
|
|
649
|
-
iReckon(sir, foundBob!.isForeign).asTo('isForeign flag').isGonnaBeTrue();
|
|
650
|
-
|
|
651
|
-
// 2. Verify Proof
|
|
652
|
-
const proof = updatedKeystone.data!.proofs[0];
|
|
653
|
-
iReckon(sir, proof.claim.verb).asTo('proof verb').willEqual("manage");
|
|
654
|
-
// Alice signed this using HER pool (default), not Bob's
|
|
655
|
-
iReckon(sir, proof.solutions[0].poolId).willEqual(POOL_ID_DEFAULT);
|
|
656
|
-
|
|
657
|
-
// 3. Verify Validity
|
|
658
|
-
const errors = await service.validate({
|
|
659
|
-
prevIbGib: aliceKeystone,
|
|
660
|
-
currentIbGib: updatedKeystone,
|
|
661
|
-
});
|
|
662
|
-
iReckon(sir, errors.length).asTo('validation passed').willEqual(0);
|
|
663
|
-
|
|
664
|
-
// Update local ref for next tests
|
|
665
|
-
aliceKeystone = updatedKeystone;
|
|
666
|
-
});
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
await respecfully(sir, 'Permissions & Logic', async () => {
|
|
670
|
-
await ifWe(sir, 'fails if no pool allows "manage" verb', async () => {
|
|
671
|
-
// 1. Create a restricted keystone
|
|
672
|
-
let id = "read_only";
|
|
673
|
-
const restrictedConfig = createStandardPoolConfig({ id, salt: id });
|
|
674
|
-
restrictedConfig.allowedVerbs = ['read']; // No 'manage'
|
|
675
|
-
|
|
676
|
-
const restrictedKeystone = await service.genesis({
|
|
677
|
-
masterSecret: aliceSecret,
|
|
678
|
-
configs: [restrictedConfig],
|
|
679
|
-
metaspace: mockMetaspace,
|
|
680
|
-
space: mockSpace as any,
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
const newPool = await createForeignPool("pool_test");
|
|
684
|
-
let errorCaught = false;
|
|
685
|
-
|
|
686
|
-
try {
|
|
687
|
-
await service.addPools({
|
|
688
|
-
latestKeystone: restrictedKeystone,
|
|
689
|
-
masterSecret: aliceSecret,
|
|
690
|
-
newPools: [newPool],
|
|
691
|
-
metaspace: mockMetaspace,
|
|
692
|
-
space: mockSpace as any,
|
|
693
|
-
});
|
|
694
|
-
} catch (e: any) {
|
|
695
|
-
errorCaught = true;
|
|
696
|
-
// Should fail in resolveTargetPool or addPools logic
|
|
697
|
-
// console.log("Caught expected error:", e.message);
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
iReckon(sir, errorCaught).asTo('permission denied').isGonnaBeTrue();
|
|
701
|
-
});
|
|
1896
|
+
// Another native/non-foreign pool
|
|
1897
|
+
const secondaryNativeConfig = createStandardPoolConfig({ id: "secondary-native", salt: secondarySalt });
|
|
1898
|
+
secondaryNativeConfig.allowedVerbs = [KEYSTONE_VERB_SYNC];
|
|
702
1899
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1900
|
+
// A custodian/foreign pool
|
|
1901
|
+
const custodianManageConfig = createStandardPoolConfig({ id: POOL_ID_CUSTODIAN_MANAGE, salt: custodianSalt });
|
|
1902
|
+
custodianManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
706
1903
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
latestKeystone: aliceKeystone, // This already has pool_bob
|
|
711
|
-
masterSecret: aliceSecret,
|
|
712
|
-
newPools: [duplicatePool],
|
|
713
|
-
metaspace: mockMetaspace,
|
|
714
|
-
space: mockSpace as any,
|
|
715
|
-
});
|
|
716
|
-
} catch (e: any) {
|
|
717
|
-
errorCaught = true;
|
|
718
|
-
iReckon(sir, e.message).includes("ID collision");
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
iReckon(sir, errorCaught).asTo('collision detected').isGonnaBeTrue();
|
|
722
|
-
});
|
|
723
|
-
});
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
// ===========================================================================
|
|
727
|
-
// SUITE E: STRUCTURAL EVOLUTION (addPools)
|
|
728
|
-
// ===========================================================================
|
|
729
|
-
|
|
730
|
-
await respecfully(sir, 'Suite E: Structural Evolution (addPools)', async () => {
|
|
731
|
-
|
|
732
|
-
const service = new KeystoneService_V1();
|
|
733
|
-
const aliceSecret = "Alice_Master_Key";
|
|
734
|
-
const bobSecret = "Bob_Foreign_Key";
|
|
735
|
-
|
|
736
|
-
let mockSpace: MockIbGibSpace;
|
|
737
|
-
let mockMetaspace: any;
|
|
738
|
-
let aliceKeystone: KeystoneIbGib_V1;
|
|
739
|
-
|
|
740
|
-
// Helper to simulate Bob creating a pool "offline" to give to Alice
|
|
741
|
-
const createForeignPool = async (id: string, verbs: string[] = []): Promise<KeystoneChallengePool> => {
|
|
742
|
-
const salt = (await getUUID()).substring(0, 16);
|
|
743
|
-
const config = createStandardPoolConfig({ id, salt });
|
|
744
|
-
config.allowedVerbs = verbs;
|
|
745
|
-
|
|
746
|
-
// We use the factory manually here to simulate Bob doing this on his own machine
|
|
747
|
-
const strategy = KeystoneStrategyFactory.create({ config });
|
|
748
|
-
const poolSecret = await strategy.derivePoolSecret({ masterSecret: bobSecret });
|
|
1904
|
+
// Custodian Pool genesis
|
|
1905
|
+
const strategy = KeystoneStrategyFactory.create({ config: custodianManageConfig });
|
|
1906
|
+
const poolSecret = await strategy.derivePoolSecret({ masterSecret: custodianSecret });
|
|
749
1907
|
const challenges: { [id: string]: any } = {};
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
for (let i = 0; i < 10; i++) {
|
|
753
|
-
const raw = await hash({ s: `${config.salt}${Date.now()}${i}` });
|
|
1908
|
+
for (let i = 0; i < custodianManageConfig.behavior.size; i++) {
|
|
1909
|
+
const raw = await hash({ s: `${custodianManageConfig.salt}${Date.now()}${i}` });
|
|
754
1910
|
const challengeId = raw.substring(0, 16);
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
poolSecret, poolId: config.salt, challengeId,
|
|
758
|
-
});
|
|
759
|
-
const challenge = await strategy.generateChallenge({ solution });
|
|
760
|
-
challenges[challengeId] = challenge;
|
|
1911
|
+
const solution = await strategy.generateSolution({ poolSecret, poolId: custodianManageConfig.id, challengeId });
|
|
1912
|
+
challenges[challengeId] = await strategy.generateChallenge({ solution });
|
|
761
1913
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
config,
|
|
1914
|
+
const custodianPool: KeystoneChallengePool = {
|
|
1915
|
+
id: POOL_ID_CUSTODIAN_MANAGE,
|
|
1916
|
+
config: custodianManageConfig,
|
|
766
1917
|
challenges,
|
|
767
|
-
isForeign: true
|
|
768
|
-
metadata: { owner: 'Bob', role: 'Delegate' }
|
|
1918
|
+
isForeign: true
|
|
769
1919
|
};
|
|
770
|
-
};
|
|
771
|
-
|
|
772
|
-
firstOfAll(sir, async () => {
|
|
773
|
-
mockSpace = new MockIbGibSpace();
|
|
774
|
-
mockMetaspace = new MockMetaspaceService(mockSpace);
|
|
775
|
-
|
|
776
|
-
// Alice Genesis: Standard pool (allows all verbs, including 'manage')
|
|
777
|
-
const salt = (await getUUID()).substring(0, 16);
|
|
778
|
-
const config = createStandardPoolConfig({
|
|
779
|
-
id: POOL_ID_DEFAULT,
|
|
780
|
-
salt,
|
|
781
|
-
});
|
|
782
|
-
aliceKeystone = await service.genesis({
|
|
783
|
-
masterSecret: aliceSecret,
|
|
784
|
-
configs: [config],
|
|
785
|
-
metaspace: mockMetaspace,
|
|
786
|
-
space: mockSpace as any,
|
|
787
|
-
});
|
|
788
|
-
});
|
|
789
|
-
|
|
790
|
-
await respecfully(sir, 'Happy Path', async () => {
|
|
791
|
-
await ifWe(sir, 'authorizes and adds a foreign pool', async () => {
|
|
792
|
-
const bobPool = await createForeignPool("pool_bob", ["post"]);
|
|
793
|
-
|
|
794
|
-
const updatedKeystone = await service.addPools({
|
|
795
|
-
latestKeystone: aliceKeystone,
|
|
796
|
-
masterSecret: aliceSecret,
|
|
797
|
-
newPools: [bobPool],
|
|
798
|
-
metaspace: mockMetaspace,
|
|
799
|
-
space: mockSpace as any,
|
|
800
|
-
});
|
|
801
|
-
|
|
802
|
-
// 1. Verify new state
|
|
803
|
-
iReckon(sir, updatedKeystone).isGonnaBeTruthy();
|
|
804
|
-
const pools = updatedKeystone.data!.challengePools;
|
|
805
|
-
iReckon(sir, pools.length).asTo('pool count increased').willEqual(2);
|
|
806
|
-
|
|
807
|
-
const foundBob = pools.find(p => p.id === "pool_bob");
|
|
808
|
-
iReckon(sir, foundBob).asTo('bob pool exists').isGonnaBeTruthy();
|
|
809
|
-
iReckon(sir, foundBob!.isForeign).asTo('isForeign flag').isGonnaBeTrue();
|
|
810
1920
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
// Alice signed this using HER pool (default), not Bob's
|
|
815
|
-
iReckon(sir, proof.solutions[0].poolId).willEqual(POOL_ID_DEFAULT);
|
|
816
|
-
|
|
817
|
-
// 3. Verify Validity
|
|
818
|
-
const errors = await service.validate({
|
|
819
|
-
prevIbGib: aliceKeystone,
|
|
820
|
-
currentIbGib: updatedKeystone,
|
|
821
|
-
});
|
|
822
|
-
iReckon(sir, errors.length).asTo('validation passed').willEqual(0);
|
|
823
|
-
|
|
824
|
-
// Update local ref for next tests
|
|
825
|
-
aliceKeystone = updatedKeystone;
|
|
826
|
-
});
|
|
827
|
-
});
|
|
828
|
-
|
|
829
|
-
await respecfully(sir, 'Permissions & Logic', async () => {
|
|
830
|
-
await ifWe(sir, 'fails if no pool allows "manage" verb', async () => {
|
|
831
|
-
// 1. Create a restricted keystone (read-only)
|
|
832
|
-
let id = "read_only";
|
|
833
|
-
const restrictedConfig = createStandardPoolConfig({ id, salt: id });
|
|
834
|
-
restrictedConfig.allowedVerbs = ['read']; // No 'manage'
|
|
835
|
-
|
|
836
|
-
const restrictedKeystone = await service.genesis({
|
|
837
|
-
masterSecret: aliceSecret,
|
|
838
|
-
configs: [restrictedConfig],
|
|
839
|
-
metaspace: mockMetaspace,
|
|
840
|
-
space: mockSpace as any,
|
|
841
|
-
});
|
|
842
|
-
|
|
843
|
-
const newPool = await createForeignPool("pool_test");
|
|
844
|
-
let errorCaught = false;
|
|
845
|
-
|
|
846
|
-
try {
|
|
847
|
-
await service.addPools({
|
|
848
|
-
latestKeystone: restrictedKeystone,
|
|
849
|
-
masterSecret: aliceSecret,
|
|
850
|
-
newPools: [newPool],
|
|
851
|
-
metaspace: mockMetaspace,
|
|
852
|
-
space: mockSpace as any,
|
|
853
|
-
});
|
|
854
|
-
} catch (e: any) {
|
|
855
|
-
errorCaught = true;
|
|
856
|
-
// Optional: Check error message
|
|
857
|
-
// iReckon(sir, e.message).includes("No local pool found with 'manage'");
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
iReckon(sir, errorCaught).asTo('permission denied').isGonnaBeTrue();
|
|
861
|
-
});
|
|
862
|
-
|
|
863
|
-
await ifWe(sir, 'fails on ID collision', async () => {
|
|
864
|
-
// Try to add "pool_bob" again (it was added in Happy Path)
|
|
865
|
-
const duplicatePool = await createForeignPool("pool_bob");
|
|
866
|
-
|
|
867
|
-
let errorCaught = false;
|
|
868
|
-
try {
|
|
869
|
-
await service.addPools({
|
|
870
|
-
latestKeystone: aliceKeystone, // This already has pool_bob
|
|
871
|
-
masterSecret: aliceSecret,
|
|
872
|
-
newPools: [duplicatePool],
|
|
873
|
-
metaspace: mockMetaspace,
|
|
874
|
-
space: mockSpace as any,
|
|
875
|
-
});
|
|
876
|
-
} catch (e: any) {
|
|
877
|
-
errorCaught = true;
|
|
878
|
-
iReckon(sir, e.message).includes("collision");
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
iReckon(sir, errorCaught).asTo('collision detected').isGonnaBeTrue();
|
|
882
|
-
});
|
|
883
|
-
});
|
|
884
|
-
});
|
|
885
|
-
|
|
886
|
-
// ===========================================================================
|
|
887
|
-
// SUITE F: DEEP INSPECTION (Granularity & Serialization)
|
|
888
|
-
// ===========================================================================
|
|
889
|
-
|
|
890
|
-
await respecfully(sir, 'Suite F: Deep Inspection', async () => {
|
|
891
|
-
|
|
892
|
-
const service = new KeystoneService_V1();
|
|
893
|
-
const aliceSecret = "Alice_Deep_Inspect";
|
|
894
|
-
const poolId = "granularity_pool";
|
|
895
|
-
|
|
896
|
-
let mockSpace: MockIbGibSpace;
|
|
897
|
-
let mockMetaspace: any;
|
|
898
|
-
let genesisKeystone: KeystoneIbGib_V1;
|
|
899
|
-
let signedKeystone: KeystoneIbGib_V1;
|
|
900
|
-
let hybridConfig: KeystonePoolConfig_HashV1;
|
|
901
|
-
|
|
902
|
-
firstOfAll(sir, async () => {
|
|
903
|
-
mockSpace = new MockIbGibSpace();
|
|
904
|
-
mockMetaspace = new MockMetaspaceService(mockSpace);
|
|
905
|
-
|
|
906
|
-
const salt = (await getUUID()).substring(0, 16);
|
|
907
|
-
hybridConfig = createStandardPoolConfig({
|
|
908
|
-
id: poolId,
|
|
909
|
-
salt,
|
|
910
|
-
// 2 FIFO + 2 Random = 4 Total per sign
|
|
911
|
-
sequential: 2, random: 2, targetBinding: 0,
|
|
912
|
-
size: 20, // Small enough to track, large enough to be random
|
|
913
|
-
}) as KeystonePoolConfig_HashV1;
|
|
914
|
-
|
|
915
|
-
genesisKeystone = await service.genesis({
|
|
916
|
-
masterSecret: aliceSecret,
|
|
917
|
-
configs: [hybridConfig],
|
|
918
|
-
metaspace: mockMetaspace,
|
|
919
|
-
space: mockSpace as any,
|
|
920
|
-
});
|
|
921
|
-
});
|
|
922
|
-
|
|
923
|
-
await respecfully(sir, 'Proof Granularity & Math', async () => {
|
|
924
|
-
|
|
925
|
-
await ifWe(sir, 'generates exactly the expected number of solutions', async () => {
|
|
926
|
-
signedKeystone = await service.sign({
|
|
927
|
-
latestKeystone: genesisKeystone,
|
|
928
|
-
masterSecret: aliceSecret,
|
|
929
|
-
claim: { verb: "post", target: "data^gib" },
|
|
930
|
-
metaspace: mockMetaspace,
|
|
931
|
-
space: mockSpace as any,
|
|
932
|
-
});
|
|
933
|
-
|
|
934
|
-
const proofs = signedKeystone.data!.proofs;
|
|
935
|
-
iReckon(sir, proofs.length).asTo('proof count').willEqual(1);
|
|
936
|
-
|
|
937
|
-
const solutions = proofs[0].solutions;
|
|
938
|
-
// 2 Sequential + 2 Random = 4
|
|
939
|
-
iReckon(sir, solutions.length).asTo('solution count').willEqual(4);
|
|
940
|
-
});
|
|
941
|
-
|
|
942
|
-
await ifWe(sir, 'verifies the math manually (White-box Crypto Check)', async () => {
|
|
943
|
-
const proof = signedKeystone.data!.proofs[0];
|
|
944
|
-
const poolSnapshot = genesisKeystone.data!.challengePools.find(p => p.id === poolId)!;
|
|
945
|
-
|
|
946
|
-
// We iterate every solution in the proof and MANUALLY verify the hash relationship
|
|
947
|
-
// bypassing the Service's validation logic to ensure the raw math holds up.
|
|
948
|
-
|
|
949
|
-
for (const solution of proof.solutions) {
|
|
950
|
-
// 1. Find the challenge in the *Previous* frame (Genesis)
|
|
951
|
-
const challenge = poolSnapshot.challenges[solution.challengeId];
|
|
952
|
-
|
|
953
|
-
if (!challenge) {
|
|
954
|
-
throw new Error(`Test Failure: Solution references ID ${solution.challengeId} which was not in Genesis pool.`);
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// 2. Re-implement HashReveal V1 verification logic locally in the test
|
|
958
|
-
// Hash(Salt + Value + Salt)
|
|
959
|
-
// Note: rounds=1 in standard config
|
|
960
|
-
const indexSalt = solution.challengeId;
|
|
961
|
-
const calculatedHash = await hash({
|
|
962
|
-
s: `${indexSalt}${solution.value}${indexSalt}`,
|
|
963
|
-
algorithm: 'SHA-256'
|
|
964
|
-
});
|
|
965
|
-
|
|
966
|
-
// 3. Assert
|
|
967
|
-
iReckon(sir, calculatedHash).asTo(`Manual hash verification for ${solution.challengeId}`).willEqual(challenge.hash);
|
|
968
|
-
}
|
|
969
|
-
});
|
|
970
|
-
|
|
971
|
-
await ifWe(sir, 'verifies FIFO logic (Deterministic Selection)', async () => {
|
|
972
|
-
const proof = signedKeystone.data!.proofs[0];
|
|
973
|
-
const poolSnapshot = genesisKeystone.data!.challengePools.find(p => p.id === poolId)!;
|
|
974
|
-
|
|
975
|
-
// The first N keys in the pool should be the FIFO targets.
|
|
976
|
-
// Assumption: Object.keys returns insertion order (Standard in modern JS engines)
|
|
977
|
-
const allIds = Object.keys(poolSnapshot.challenges);
|
|
978
|
-
const expectedFifoIds = allIds.slice(0, 2);
|
|
979
|
-
|
|
980
|
-
const solvedIds = proof.solutions.map(s => s.challengeId);
|
|
981
|
-
|
|
982
|
-
// Check that our solution list *includes* the expected FIFO IDs
|
|
983
|
-
const hasFirst = solvedIds.includes(expectedFifoIds[0]);
|
|
984
|
-
const hasSecond = solvedIds.includes(expectedFifoIds[1]);
|
|
985
|
-
|
|
986
|
-
iReckon(sir, hasFirst).asTo(`Solution includes 1st FIFO ID (${expectedFifoIds[0]})`).isGonnaBeTrue();
|
|
987
|
-
iReckon(sir, hasSecond).asTo(`Solution includes 2nd FIFO ID (${expectedFifoIds[1]})`).isGonnaBeTrue();
|
|
988
|
-
});
|
|
989
|
-
|
|
990
|
-
await ifWe(sir, 'verifies Next-Gen Hash-Based Target Binding Selection', async () => {
|
|
991
|
-
const bindingConfig = createStandardPoolConfig({
|
|
992
|
-
id: "binding_pool",
|
|
993
|
-
salt: "binding_salt",
|
|
994
|
-
sequential: 0,
|
|
995
|
-
random: 0,
|
|
996
|
-
targetBinding: 3,
|
|
997
|
-
size: 20
|
|
998
|
-
}) as KeystonePoolConfig_HashV1;
|
|
999
|
-
|
|
1000
|
-
const tempGenesis = await service.genesis({
|
|
1001
|
-
masterSecret: aliceSecret,
|
|
1002
|
-
configs: [bindingConfig],
|
|
1003
|
-
metaspace: mockMetaspace,
|
|
1004
|
-
space: mockSpace as any,
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
// Evolve using the binding pool
|
|
1008
|
-
const evolved = await service.sign({
|
|
1009
|
-
latestKeystone: tempGenesis,
|
|
1010
|
-
masterSecret: aliceSecret,
|
|
1011
|
-
poolId: "binding_pool",
|
|
1012
|
-
claim: {
|
|
1013
|
-
verb: "sign",
|
|
1014
|
-
target: "some_target_address^gib"
|
|
1015
|
-
},
|
|
1016
|
-
metaspace: mockMetaspace,
|
|
1017
|
-
space: mockSpace as any,
|
|
1018
|
-
});
|
|
1019
|
-
|
|
1020
|
-
const proof = evolved.data!.proofs[0];
|
|
1021
|
-
iReckon(sir, proof.solutions.length).asTo('proof solutions count').willEqual(3);
|
|
1022
|
-
|
|
1023
|
-
// Now, let's verify that the selection is perfectly deterministic.
|
|
1024
|
-
// If we run selectChallengeIds manually with the same pool and target, it must return the same IDs.
|
|
1025
|
-
const poolSnapshot = tempGenesis.data!.challengePools.find(p => p.id === "binding_pool")!;
|
|
1026
|
-
const selectedIds = await selectChallengeIds({
|
|
1027
|
-
pool: poolSnapshot,
|
|
1028
|
-
targetAddr: "some_target_address^gib"
|
|
1029
|
-
});
|
|
1030
|
-
|
|
1031
|
-
const proofIds = proof.solutions.map(s => s.challengeId);
|
|
1032
|
-
iReckon(sir, selectedIds.length).asTo('selected IDs count').willEqual(3);
|
|
1033
|
-
for (const id of selectedIds) {
|
|
1034
|
-
iReckon(sir, proofIds.includes(id)).asTo(`proof includes selected ID ${id}`).isGonnaBeTrue();
|
|
1035
|
-
}
|
|
1036
|
-
});
|
|
1037
|
-
|
|
1038
|
-
await ifWe(sir, 'fails validation if total requested challenges >= size', async () => {
|
|
1039
|
-
const invalidConfig = createStandardPoolConfig({
|
|
1040
|
-
id: "invalid_pool",
|
|
1041
|
-
salt: "invalid_salt",
|
|
1042
|
-
sequential: 5,
|
|
1043
|
-
random: 5,
|
|
1044
|
-
targetBinding: 6,
|
|
1045
|
-
size: 15 // total requested is 5+5+6 = 16 >= 15
|
|
1046
|
-
});
|
|
1047
|
-
|
|
1048
|
-
const errors = await validateChallengePool({ pool: { id: "invalid_pool", config: invalidConfig, challenges: {} } });
|
|
1049
|
-
iReckon(sir, errors.length > 0).asTo('errors length is positive').isGonnaBeTrue();
|
|
1050
|
-
const hasCorrectError = errors.some(e => e.includes("Total requested challenges"));
|
|
1051
|
-
iReckon(sir, hasCorrectError).asTo('validation error content').isGonnaBeTrue();
|
|
1052
|
-
});
|
|
1053
|
-
});
|
|
1054
|
-
|
|
1055
|
-
await respecfully(sir, 'DTO & Serialization', async () => {
|
|
1056
|
-
|
|
1057
|
-
await ifWe(sir, 'survives a clone/JSON-cycle without corruption', async () => {
|
|
1058
|
-
// 1. Create a DTO (simulate network transmission/storage)
|
|
1059
|
-
// 'clone' does a JSON stringify/parse under the hood (usually) or structured clone.
|
|
1060
|
-
const dto = clone(signedKeystone);
|
|
1061
|
-
|
|
1062
|
-
// 2. Structural checks
|
|
1063
|
-
iReckon(sir, dto).asTo('dto exists').isGonnaBeTruthy();
|
|
1064
|
-
iReckon(sir, dto.data).asTo('dto data').isGonnaBeTruthy();
|
|
1065
|
-
iReckon(sir, dto.data!.proofs).asTo('dto proofs').isGonnaBeTruthy();
|
|
1066
|
-
|
|
1067
|
-
// 3. Functional check: Can the service validate this DTO?
|
|
1068
|
-
// This ensures no prototypes or hidden properties were lost that the service depends on.
|
|
1069
|
-
const errors = await service.validate({
|
|
1070
|
-
prevIbGib: genesisKeystone,
|
|
1071
|
-
currentIbGib: dto, // Passing the DTO, not the original object
|
|
1072
|
-
});
|
|
1073
|
-
|
|
1074
|
-
iReckon(sir, errors.length).asTo('DTO validation errors').willEqual(0);
|
|
1075
|
-
});
|
|
1076
|
-
|
|
1077
|
-
await ifWe(sir, 'ensures data contains no functions or circular refs', async () => {
|
|
1078
|
-
// A crude but effective test: ensure JSON.stringify doesn't throw
|
|
1079
|
-
// and the result is equal to the object (if we parsed it back).
|
|
1080
|
-
|
|
1081
|
-
const jsonStr = JSON.stringify(signedKeystone);
|
|
1082
|
-
const parsed = JSON.parse(jsonStr);
|
|
1083
|
-
|
|
1084
|
-
// Compare specific deep fields
|
|
1085
|
-
const originalSolution = signedKeystone.data!.proofs[0].solutions[0].value;
|
|
1086
|
-
const parsedSolution = parsed.data.proofs[0].solutions[0].value;
|
|
1087
|
-
|
|
1088
|
-
iReckon(sir, parsedSolution).asTo('deep property survives stringify').willEqual(originalSolution);
|
|
1089
|
-
|
|
1090
|
-
// Ensure no extra properties were lost
|
|
1091
|
-
// FIX: JSON.stringify removes keys with 'undefined' values.
|
|
1092
|
-
// We must filter the original keys to match this behavior for a fair comparison.
|
|
1093
|
-
const origKeys = Object.keys(signedKeystone.data!)
|
|
1094
|
-
.filter(k => (signedKeystone.data as any)[k] !== undefined);
|
|
1095
|
-
|
|
1096
|
-
const parsedKeys = Object.keys(parsed.data);
|
|
1097
|
-
iReckon(sir, parsedKeys.length).asTo('key count matches').willEqual(origKeys.length);
|
|
1098
|
-
});
|
|
1099
|
-
|
|
1100
|
-
});
|
|
1101
|
-
|
|
1102
|
-
await respecfully(sir, 'KeystoneProfileBuilder and Modular Schemas', async () => {
|
|
1103
|
-
|
|
1104
|
-
await ifWe(sir, 'compiles configs successfully with medium profile', async () => {
|
|
1105
|
-
const builder = KeystoneProfileBuilder.buildKeystone('medium')
|
|
1106
|
-
.withUsername('alice')
|
|
1107
|
-
.withEmail('alice@example.com')
|
|
1108
|
-
.withDescription('Alice test keystone')
|
|
1109
|
-
.withDetails({ extraInfo: 'abc' });
|
|
1110
|
-
|
|
1111
|
-
const configs = await builder.compileConfigs();
|
|
1112
|
-
iReckon(sir, configs.length).asTo('number of pools').willEqual(5);
|
|
1113
|
-
|
|
1114
|
-
// Verify unique salt derivation per pool
|
|
1115
|
-
const salts = configs.map(c => c.salt);
|
|
1116
|
-
const uniqueSalts = new Set(salts);
|
|
1117
|
-
iReckon(sir, uniqueSalts.size).asTo('unique salts count').willEqual(5);
|
|
1118
|
-
|
|
1119
|
-
// Verify frameDetails unification
|
|
1120
|
-
const details = builder.getFrameDetails();
|
|
1121
|
-
iReckon(sir, details.username).asTo('username').willEqual('alice');
|
|
1122
|
-
iReckon(sir, details.email).asTo('email').willEqual('alice@example.com');
|
|
1123
|
-
iReckon(sir, details.description).asTo('description').willEqual('Alice test keystone');
|
|
1124
|
-
iReckon(sir, details.extraInfo).asTo('extraInfo').willEqual('abc');
|
|
1125
|
-
});
|
|
1126
|
-
|
|
1127
|
-
await ifWe(sir, 'supports customized overrides', async () => {
|
|
1128
|
-
const builder = KeystoneProfileBuilder.buildKeystone('test')
|
|
1129
|
-
.customizePool('sync', {
|
|
1130
|
-
behavior: {
|
|
1131
|
-
size: 15,
|
|
1132
|
-
targetBindingCount: 4
|
|
1133
|
-
}
|
|
1134
|
-
});
|
|
1135
|
-
|
|
1136
|
-
const configs = await builder.compileConfigs();
|
|
1137
|
-
const syncPool = configs.find(c => c.id === 'sync')!;
|
|
1138
|
-
iReckon(sir, syncPool.behavior.size).asTo('customized size').willEqual(15);
|
|
1139
|
-
iReckon(sir, syncPool.behavior.targetBindingCount).asTo('customized target binding').willEqual(4);
|
|
1140
|
-
});
|
|
1141
|
-
|
|
1142
|
-
});
|
|
1143
|
-
|
|
1144
|
-
await respecfully(sir, 'Keystone Metadata Validation', async () => {
|
|
1145
|
-
|
|
1146
|
-
await ifWe(sir, 'validates correct username and description', async () => {
|
|
1147
|
-
const service = new KeystoneService_V1();
|
|
1148
|
-
const space = new MockIbGibSpace();
|
|
1149
|
-
const metaspace = new MockMetaspaceService(space);
|
|
1150
|
-
|
|
1151
|
-
const builder = KeystoneProfileBuilder.buildKeystone('test')
|
|
1152
|
-
.withUsername('alice_123.test')
|
|
1153
|
-
.withDescription('Valid description: yes, it is!')
|
|
1154
|
-
.withDetails({ client: 'respec' });
|
|
1155
|
-
|
|
1156
|
-
const configs = await builder.compileConfigs();
|
|
1157
|
-
const frameDetails = builder.getFrameDetails();
|
|
1158
|
-
|
|
1159
|
-
// Should succeed without error
|
|
1160
|
-
const keystoneIbGib = await service.genesis({
|
|
1161
|
-
masterSecret: 'mysecret',
|
|
1162
|
-
configs,
|
|
1163
|
-
metaspace: metaspace as any,
|
|
1164
|
-
space: space as any,
|
|
1165
|
-
frameDetails
|
|
1166
|
-
});
|
|
1167
|
-
|
|
1168
|
-
const errors = await service.validateGenesisKeystone({ keystoneIbGib });
|
|
1169
|
-
iReckon(sir, errors.length).asTo('errors count').willEqual(0);
|
|
1170
|
-
});
|
|
1171
|
-
|
|
1172
|
-
await ifWe(sir, 'fails validation for invalid username length or character', async () => {
|
|
1173
|
-
const service = new KeystoneService_V1();
|
|
1174
|
-
const space = new MockIbGibSpace();
|
|
1175
|
-
const metaspace = new MockMetaspaceService(space);
|
|
1176
|
-
|
|
1177
|
-
const builder = KeystoneProfileBuilder.buildKeystone('test')
|
|
1178
|
-
.withUsername('invalid_user_name_that_is_way_too_long_for_the_sixty_three_char_limit_and_will_fail_regex_check')
|
|
1179
|
-
.withDetails({ client: 'respec' });
|
|
1180
|
-
|
|
1181
|
-
const configs = await builder.compileConfigs();
|
|
1182
|
-
const frameDetails = builder.getFrameDetails();
|
|
1183
|
-
|
|
1184
|
-
const keystoneIbGib = await service.genesis({
|
|
1185
|
-
masterSecret: 'mysecret',
|
|
1186
|
-
configs,
|
|
1187
|
-
metaspace: metaspace as any,
|
|
1188
|
-
space: space as any,
|
|
1189
|
-
frameDetails
|
|
1190
|
-
});
|
|
1191
|
-
|
|
1192
|
-
const errors = await service.validateGenesisKeystone({ keystoneIbGib });
|
|
1193
|
-
iReckon(sir, errors.length).asTo('errors count').willEqual(1);
|
|
1194
|
-
iReckon(sir, errors[0]).includes('invalid username');
|
|
1195
|
-
});
|
|
1196
|
-
|
|
1197
|
-
await ifWe(sir, 'fails validation for invalid description characters', async () => {
|
|
1198
|
-
const service = new KeystoneService_V1();
|
|
1199
|
-
const space = new MockIbGibSpace();
|
|
1200
|
-
const metaspace = new MockMetaspaceService(space);
|
|
1201
|
-
|
|
1202
|
-
const builder = KeystoneProfileBuilder.buildKeystone('test')
|
|
1203
|
-
.withUsername('alice')
|
|
1204
|
-
.withDescription('Invalid desc with weird emoji 🚀')
|
|
1205
|
-
.withDetails({ client: 'respec' });
|
|
1206
|
-
|
|
1207
|
-
const configs = await builder.compileConfigs();
|
|
1208
|
-
const frameDetails = builder.getFrameDetails();
|
|
1209
|
-
|
|
1210
|
-
const keystoneIbGib = await service.genesis({
|
|
1211
|
-
masterSecret: 'mysecret',
|
|
1212
|
-
configs,
|
|
1213
|
-
metaspace: metaspace as any,
|
|
1214
|
-
space: space as any,
|
|
1215
|
-
frameDetails
|
|
1216
|
-
});
|
|
1217
|
-
|
|
1218
|
-
const errors = await service.validateGenesisKeystone({ keystoneIbGib });
|
|
1219
|
-
iReckon(sir, errors.length).asTo('errors count').willEqual(1);
|
|
1220
|
-
iReckon(sir, errors[0]).includes('invalid description');
|
|
1221
|
-
});
|
|
1222
|
-
|
|
1223
|
-
});
|
|
1224
|
-
});
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
// ===========================================================================
|
|
1228
|
-
// SUITE E: DELEGATION (Register/Unregister)
|
|
1229
|
-
// ===========================================================================
|
|
1230
|
-
|
|
1231
|
-
await respecfully(sir, 'Suite E: Delegation (Register/Unregister)', async () => {
|
|
1232
|
-
|
|
1233
|
-
const service = new KeystoneService_V1();
|
|
1234
|
-
const parentSecret = "ParentMasterSecret_123";
|
|
1235
|
-
const delegateSecret = "DelegateMasterSecret_456";
|
|
1236
|
-
|
|
1237
|
-
let mockSpace: MockIbGibSpace;
|
|
1238
|
-
let mockMetaspace: any;
|
|
1239
|
-
|
|
1240
|
-
let parentKeystone: KeystoneIbGib_V1;
|
|
1241
|
-
let delegateKeystone: KeystoneIbGib_V1;
|
|
1242
|
-
|
|
1243
|
-
firstOfEach(sir, async () => {
|
|
1244
|
-
mockSpace = new MockIbGibSpace();
|
|
1245
|
-
mockMetaspace = new MockMetaspaceService(mockSpace);
|
|
1246
|
-
|
|
1247
|
-
const salt = (await getUUID()).substring(0, 16);
|
|
1248
|
-
const defaultConfig = createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt });
|
|
1249
|
-
const manageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: salt + "_manage" });
|
|
1250
|
-
manageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE];
|
|
1251
|
-
|
|
1252
|
-
// 1. Create Parent Keystone (with manage pool)
|
|
1253
|
-
parentKeystone = await service.genesis({
|
|
1254
|
-
masterSecret: parentSecret,
|
|
1255
|
-
configs: [defaultConfig, manageConfig],
|
|
1921
|
+
const keystone = await service.genesis({
|
|
1922
|
+
masterSecret: userSecret,
|
|
1923
|
+
configs: [userManageConfig, secondaryNativeConfig],
|
|
1256
1924
|
metaspace: mockMetaspace,
|
|
1257
1925
|
space: mockSpace as any,
|
|
1258
1926
|
});
|
|
1259
1927
|
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
configs: [delegateConfig],
|
|
1928
|
+
const keystoneWithCustodian = await service.addPools({
|
|
1929
|
+
latestKeystone: keystone,
|
|
1930
|
+
masterSecret: userSecret,
|
|
1931
|
+
newPools: [custodianPool],
|
|
1265
1932
|
metaspace: mockMetaspace,
|
|
1266
1933
|
space: mockSpace as any,
|
|
1267
1934
|
});
|
|
1268
|
-
});
|
|
1269
1935
|
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
const
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1936
|
+
// Rotate passphrase/password
|
|
1937
|
+
const newPassphrase = "SuperNewSecretPassphrase_999";
|
|
1938
|
+
const rotated = await service.changePassword({
|
|
1939
|
+
latestKeystone: keystoneWithCustodian,
|
|
1940
|
+
newMasterSecret: newPassphrase,
|
|
1941
|
+
signingSecret: userSecret,
|
|
1942
|
+
signingPoolId: POOL_ID_MANAGE,
|
|
1276
1943
|
metaspace: mockMetaspace,
|
|
1277
1944
|
space: mockSpace as any,
|
|
1278
|
-
allowedVerbs: [KEYSTONE_VERB_SYNC],
|
|
1279
1945
|
});
|
|
1280
1946
|
|
|
1281
|
-
iReckon(sir,
|
|
1282
|
-
iReckon(sir, evolvedParent.data?.delegates).asTo('delegates map exists').isGonnaBeTruthy();
|
|
1947
|
+
iReckon(sir, rotated).isGonnaBeTruthy();
|
|
1283
1948
|
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
iReckon(sir, delegateInfo?.delegateTjpAddr).asTo('delegate tjp matches').willEqual(delegateTjpAddr);
|
|
1289
|
-
});
|
|
1949
|
+
const data = rotated.data!;
|
|
1950
|
+
const rotatedManage = data.challengePools.find(p => p.id === POOL_ID_MANAGE)!;
|
|
1951
|
+
const rotatedSecondary = data.challengePools.find(p => p.id === "secondary-native")!;
|
|
1952
|
+
const rotatedCustodian = data.challengePools.find(p => p.id === POOL_ID_CUSTODIAN_MANAGE)!;
|
|
1290
1953
|
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone });
|
|
1295
|
-
if (!delegateTjpAddr) { throw new Error(`(UNEXPECTED) delegateTjpAddr falsy? (E: 3e4878ea19dd8034e827466a8bf09526)`); }
|
|
1954
|
+
// Verify native pools rotated (salt changed)
|
|
1955
|
+
iReckon(sir, rotatedManage.config.salt).not.willEqual(userSalt);
|
|
1956
|
+
iReckon(sir, rotatedSecondary.config.salt).not.willEqual(secondarySalt);
|
|
1296
1957
|
|
|
1297
|
-
//
|
|
1298
|
-
|
|
1299
|
-
parentTjpAddr: parentTjpAddr,
|
|
1300
|
-
delegateTjpAddr: delegateTjpAddr,
|
|
1301
|
-
metaspace: mockMetaspace,
|
|
1302
|
-
space: mockSpace as any,
|
|
1303
|
-
});
|
|
1304
|
-
iReckon(sir, statusBefore.registered).asTo('initially unregistered').isGonnaBeFalse();
|
|
1958
|
+
// Verify foreign pool was not modified (salt identical)
|
|
1959
|
+
iReckon(sir, rotatedCustodian.config.salt).willEqual(custodianSalt);
|
|
1305
1960
|
|
|
1306
|
-
//
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
masterSecret: parentSecret,
|
|
1311
|
-
metaspace: mockMetaspace,
|
|
1312
|
-
space: mockSpace as any,
|
|
1313
|
-
allowedVerbs: [KEYSTONE_VERB_SYNC],
|
|
1961
|
+
// Verify we can validate transition
|
|
1962
|
+
const errors = await service.validate({
|
|
1963
|
+
prevIbGib: keystoneWithCustodian,
|
|
1964
|
+
currentIbGib: rotated
|
|
1314
1965
|
});
|
|
1315
|
-
|
|
1316
|
-
// After registration
|
|
1317
|
-
const statusAfter = await service.getDelegateStatus({
|
|
1318
|
-
parentTjpAddr: parentTjpAddr,
|
|
1319
|
-
delegateTjpAddr: delegateTjpAddr,
|
|
1320
|
-
metaspace: mockMetaspace,
|
|
1321
|
-
space: mockSpace as any,
|
|
1322
|
-
});
|
|
1323
|
-
iReckon(sir, statusAfter.registered).asTo('registered status').isGonnaBeTrue();
|
|
1324
|
-
iReckon(sir, statusAfter.delegateInfo?.delegateTjpAddr).asTo('status tjp matches').willEqual(delegateTjpAddr);
|
|
1966
|
+
iReckon(sir, errors.length).willEqual(0);
|
|
1325
1967
|
});
|
|
1326
1968
|
|
|
1327
|
-
await
|
|
1328
|
-
//
|
|
1329
|
-
const
|
|
1330
|
-
|
|
1331
|
-
delegateKeystone,
|
|
1332
|
-
masterSecret: parentSecret,
|
|
1333
|
-
metaspace: mockMetaspace,
|
|
1334
|
-
space: mockSpace as any,
|
|
1335
|
-
allowedVerbs: [KEYSTONE_VERB_SYNC],
|
|
1336
|
-
});
|
|
1337
|
-
|
|
1338
|
-
const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone });
|
|
1339
|
-
if (!delegateTjpAddr) { throw new Error(`(UNEXPECTED) delegateTjpAddr falsy? (E: 486b486a8c984a52b8d2b22418c20d26)`); }
|
|
1340
|
-
|
|
1341
|
-
// Unregister
|
|
1342
|
-
const finalParent = await service.unregisterDelegate({
|
|
1343
|
-
parentKeystone: evolvedParent,
|
|
1344
|
-
delegateTjpAddr,
|
|
1345
|
-
masterSecret: parentSecret,
|
|
1346
|
-
metaspace: mockMetaspace,
|
|
1347
|
-
space: mockSpace as any,
|
|
1348
|
-
});
|
|
1349
|
-
|
|
1350
|
-
iReckon(sir, finalParent).asTo('final evolved parent exists').isGonnaBeTruthy();
|
|
1351
|
-
const info = finalParent.data?.delegates?.[delegateTjpAddr];
|
|
1352
|
-
iReckon(sir, info).asTo('delegate info removed').isGonnaBeUndefined();
|
|
1353
|
-
});
|
|
1969
|
+
await ifWeMight(sir, 'fails validation if password rotation details or constraints are violated', async () => {
|
|
1970
|
+
// Construct invalid change-password transition
|
|
1971
|
+
const userManageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: "salt1" });
|
|
1972
|
+
userManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
1354
1973
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
const salt = (await getUUID()).substring(0, 16);
|
|
1358
|
-
const defaultConfig = createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt });
|
|
1359
|
-
const restrictedParent = await service.genesis({
|
|
1360
|
-
masterSecret: parentSecret,
|
|
1361
|
-
configs: [defaultConfig],
|
|
1362
|
-
metaspace: mockMetaspace,
|
|
1363
|
-
space: mockSpace as any,
|
|
1364
|
-
});
|
|
1974
|
+
const custodianManageConfig = createStandardPoolConfig({ id: POOL_ID_CUSTODIAN_MANAGE, salt: "salt2" });
|
|
1975
|
+
custodianManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
1365
1976
|
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
});
|
|
1376
|
-
} catch (e: any) {
|
|
1377
|
-
errorCaught = true;
|
|
1378
|
-
iReckon(sir, e.message).includes('Manage pool is required');
|
|
1977
|
+
// Custodian Pool genesis
|
|
1978
|
+
const strategy = KeystoneStrategyFactory.create({ config: custodianManageConfig });
|
|
1979
|
+
const poolSecret = await strategy.derivePoolSecret({ masterSecret: custodianSecret });
|
|
1980
|
+
const challenges: { [id: string]: any } = {};
|
|
1981
|
+
for (let i = 0; i < custodianManageConfig.behavior.size; i++) {
|
|
1982
|
+
const raw = await hash({ s: `${custodianManageConfig.salt}${Date.now()}${i}` });
|
|
1983
|
+
const challengeId = raw.substring(0, 16);
|
|
1984
|
+
const solution = await strategy.generateSolution({ poolSecret, poolId: custodianManageConfig.id, challengeId });
|
|
1985
|
+
challenges[challengeId] = await strategy.generateChallenge({ solution });
|
|
1379
1986
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
const service = new KeystoneService_V1();
|
|
1386
|
-
const parentSecret = "ParentMasterSecret_SuiteG";
|
|
1387
|
-
const delegateSecret = "DelegateMasterSecret_SuiteG";
|
|
1388
|
-
|
|
1389
|
-
let mockSpace: MockIbGibSpace;
|
|
1390
|
-
let mockMetaspace: any;
|
|
1391
|
-
|
|
1392
|
-
let parentKeystone: KeystoneIbGib_V1;
|
|
1393
|
-
let delegateKeystone: KeystoneIbGib_V1;
|
|
1394
|
-
|
|
1395
|
-
firstOfEach(sir, async () => {
|
|
1396
|
-
mockSpace = new MockIbGibSpace();
|
|
1397
|
-
mockMetaspace = new MockMetaspaceService(mockSpace);
|
|
1398
|
-
|
|
1399
|
-
const salt1 = (await getUUID()).substring(0, 16);
|
|
1400
|
-
const salt2 = (await getUUID()).substring(0, 16);
|
|
1401
|
-
|
|
1402
|
-
const manageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: salt2 });
|
|
1403
|
-
manageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE];
|
|
1404
|
-
|
|
1405
|
-
const configs = [
|
|
1406
|
-
createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt: salt1 }),
|
|
1407
|
-
manageConfig
|
|
1408
|
-
];
|
|
1409
|
-
|
|
1410
|
-
parentKeystone = await service.genesis({
|
|
1411
|
-
masterSecret: parentSecret,
|
|
1412
|
-
configs,
|
|
1413
|
-
metaspace: mockMetaspace,
|
|
1414
|
-
space: mockSpace as any,
|
|
1415
|
-
});
|
|
1416
|
-
|
|
1417
|
-
delegateKeystone = await service.genesis({
|
|
1418
|
-
masterSecret: delegateSecret,
|
|
1419
|
-
configs: [createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt: salt1 })],
|
|
1420
|
-
metaspace: mockMetaspace,
|
|
1421
|
-
space: mockSpace as any,
|
|
1422
|
-
});
|
|
1423
|
-
});
|
|
1424
|
-
|
|
1425
|
-
await ifWe(sir, 'validateKeystoneIb returns true for valid keystone ib and false for others', async () => {
|
|
1426
|
-
iReckon(sir, await validateKeystoneIb({ ib: 'keystone' })).isGonnaBeTrue();
|
|
1427
|
-
iReckon(sir, await validateKeystoneIb({ ib: 'keystone user info' })).isGonnaBeTrue();
|
|
1428
|
-
iReckon(sir, await validateKeystoneIb({ ib: 'invalidAtom' })).isGonnaBeFalse();
|
|
1429
|
-
iReckon(sir, await validateKeystoneIb({ ib: 'not_keystone sub_atom' })).isGonnaBeFalse();
|
|
1430
|
-
});
|
|
1431
|
-
|
|
1432
|
-
await ifWe(sir, 'validateGenesisKeystone fails validation if ib is invalid', async () => {
|
|
1433
|
-
const invalidGenesis = {
|
|
1434
|
-
...parentKeystone,
|
|
1435
|
-
ib: 'not_a_keystone^' + parentKeystone.gib,
|
|
1987
|
+
const custodianPool: KeystoneChallengePool = {
|
|
1988
|
+
id: POOL_ID_CUSTODIAN_MANAGE,
|
|
1989
|
+
config: custodianManageConfig,
|
|
1990
|
+
challenges,
|
|
1991
|
+
isForeign: true
|
|
1436
1992
|
};
|
|
1437
|
-
const errors = await validateGenesisKeystone({ keystoneIbGib: invalidGenesis });
|
|
1438
|
-
iReckon(sir, errors.length > 0).isGonnaBeTrue();
|
|
1439
|
-
iReckon(sir, errors[0]).includes('invalid keystone ib');
|
|
1440
|
-
});
|
|
1441
1993
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
delegateKeystone,
|
|
1446
|
-
masterSecret: parentSecret,
|
|
1994
|
+
const keystone = await service.genesis({
|
|
1995
|
+
masterSecret: userSecret,
|
|
1996
|
+
configs: [userManageConfig],
|
|
1447
1997
|
metaspace: mockMetaspace,
|
|
1448
1998
|
space: mockSpace as any,
|
|
1449
|
-
allowedVerbs: [KEYSTONE_VERB_SYNC],
|
|
1450
|
-
});
|
|
1451
|
-
|
|
1452
|
-
const errors = await validateKeystoneTransition({
|
|
1453
|
-
currentIbGib: evolvedParent,
|
|
1454
|
-
prevIbGib: parentKeystone,
|
|
1455
1999
|
});
|
|
1456
|
-
iReckon(sir, errors.length).willEqual(0);
|
|
1457
|
-
});
|
|
1458
2000
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
masterSecret: parentSecret,
|
|
2001
|
+
const keystoneWithCustodian = await service.addPools({
|
|
2002
|
+
latestKeystone: keystone,
|
|
2003
|
+
masterSecret: userSecret,
|
|
2004
|
+
newPools: [custodianPool],
|
|
1464
2005
|
metaspace: mockMetaspace,
|
|
1465
2006
|
space: mockSpace as any,
|
|
1466
|
-
allowedVerbs: [KEYSTONE_VERB_SYNC],
|
|
1467
2007
|
});
|
|
1468
2008
|
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
if (proof.claim) {
|
|
1474
|
-
proof.claim.verb = 'sign';
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
const errors = await validateKeystoneTransition({
|
|
1479
|
-
currentIbGib: tamperedParent,
|
|
1480
|
-
prevIbGib: parentKeystone,
|
|
2009
|
+
// Create an invalid frame manually where a foreign pool is rotated during change-password
|
|
2010
|
+
const badRotatedPool = await rotatePoolChallenges({
|
|
2011
|
+
prevPool: custodianPool,
|
|
2012
|
+
newMasterSecret: "bad_secret"
|
|
1481
2013
|
});
|
|
1482
|
-
iReckon(sir, errors.length > 0).isGonnaBeTrue();
|
|
1483
|
-
iReckon(sir, errors.some(e => e.includes('Policy Violation: Delegates map was mutated'))).isGonnaBeTrue();
|
|
1484
|
-
});
|
|
1485
|
-
|
|
1486
|
-
await ifWe(sir, 'validateKeystoneTransition fails if delegate key does not match delegateTjpAddr', async () => {
|
|
1487
|
-
const evolvedParent = await service.registerDelegate({
|
|
1488
|
-
parentKeystone,
|
|
1489
|
-
delegateKeystone,
|
|
1490
|
-
masterSecret: parentSecret,
|
|
1491
|
-
metaspace: mockMetaspace,
|
|
1492
|
-
space: mockSpace as any,
|
|
1493
|
-
allowedVerbs: [KEYSTONE_VERB_SYNC],
|
|
1494
|
-
});
|
|
1495
|
-
|
|
1496
|
-
const tamperedParent = JSON.parse(JSON.stringify(evolvedParent)) as KeystoneIbGib_V1;
|
|
1497
|
-
const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone });
|
|
1498
|
-
if (!delegateTjpAddr) { throw new Error(`delegateTjpAddr falsy?`); }
|
|
1499
2014
|
|
|
1500
|
-
const
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
currentIbGib: tamperedParent,
|
|
1506
|
-
prevIbGib: parentKeystone,
|
|
1507
|
-
});
|
|
1508
|
-
iReckon(sir, errors.length > 0).isGonnaBeTrue();
|
|
1509
|
-
iReckon(sir, errors.some(e => e.includes('does not match delegateTjpAddr'))).isGonnaBeTrue();
|
|
1510
|
-
});
|
|
1511
|
-
|
|
1512
|
-
await ifWe(sir, 'validateKeystoneTransition fails if delegate addresses have invalid keystone ib', async () => {
|
|
1513
|
-
const evolvedParent = await service.registerDelegate({
|
|
1514
|
-
parentKeystone,
|
|
1515
|
-
delegateKeystone,
|
|
1516
|
-
masterSecret: parentSecret,
|
|
2015
|
+
const evolved = await service.changePassword({
|
|
2016
|
+
latestKeystone: keystoneWithCustodian,
|
|
2017
|
+
newMasterSecret: "new_pass",
|
|
2018
|
+
signingSecret: userSecret,
|
|
2019
|
+
signingPoolId: POOL_ID_MANAGE,
|
|
1517
2020
|
metaspace: mockMetaspace,
|
|
1518
2021
|
space: mockSpace as any,
|
|
1519
|
-
allowedVerbs: [KEYSTONE_VERB_SYNC],
|
|
1520
2022
|
});
|
|
1521
2023
|
|
|
1522
|
-
|
|
1523
|
-
const
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
2024
|
+
// Inject the foreign pool rotation manually to simulate an attacker
|
|
2025
|
+
const attackerData = {
|
|
2026
|
+
...evolved.data!,
|
|
2027
|
+
challengePools: evolved.data!.challengePools.map(p => p.id === POOL_ID_CUSTODIAN_MANAGE ? badRotatedPool : p)
|
|
2028
|
+
};
|
|
2029
|
+
// Update details claim to say we rotated custodian-manage
|
|
2030
|
+
attackerData.proofs[0].claim.details = {
|
|
2031
|
+
type: KeystoneClaimType.change_password,
|
|
2032
|
+
info: { change: [POOL_ID_MANAGE, POOL_ID_CUSTODIAN_MANAGE] } as ClaimDetails_ChangePassword
|
|
2033
|
+
};
|
|
1527
2034
|
|
|
1528
|
-
const
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
}
|
|
1532
|
-
iReckon(sir, errors.length > 0).isGonnaBeTrue();
|
|
1533
|
-
iReckon(sir, errors.some(e => e.includes('not a valid keystone address'))).isGonnaBeTrue();
|
|
1534
|
-
});
|
|
2035
|
+
const attackerKeystone = {
|
|
2036
|
+
...evolved,
|
|
2037
|
+
data: attackerData
|
|
2038
|
+
};
|
|
1535
2039
|
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
delegateKeystone,
|
|
1540
|
-
masterSecret: parentSecret,
|
|
1541
|
-
metaspace: mockMetaspace,
|
|
1542
|
-
space: mockSpace as any,
|
|
1543
|
-
allowedVerbs: [KEYSTONE_VERB_SYNC],
|
|
2040
|
+
const errors = await service.validate({
|
|
2041
|
+
prevIbGib: keystoneWithCustodian,
|
|
2042
|
+
currentIbGib: attackerKeystone
|
|
1544
2043
|
});
|
|
1545
2044
|
|
|
1546
|
-
const tamperedParent = JSON.parse(JSON.stringify(evolvedParent)) as KeystoneIbGib_V1;
|
|
1547
|
-
const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone });
|
|
1548
|
-
if (!delegateTjpAddr) { throw new Error(`delegateTjpAddr falsy?`); }
|
|
1549
|
-
|
|
1550
|
-
tamperedParent.data.delegates![delegateTjpAddr].thisAddr = 'keystone^randomgib';
|
|
1551
|
-
|
|
1552
|
-
const errors = await validateKeystoneTransition({
|
|
1553
|
-
currentIbGib: tamperedParent,
|
|
1554
|
-
prevIbGib: parentKeystone,
|
|
1555
|
-
});
|
|
1556
2045
|
iReckon(sir, errors.length > 0).isGonnaBeTrue();
|
|
1557
|
-
|
|
2046
|
+
const hasForeignViolation = errors.some(e => e.includes("Security Violation") && e.includes("foreign pool"));
|
|
2047
|
+
iReckon(sir, hasForeignViolation).isGonnaBeTrue();
|
|
1558
2048
|
});
|
|
1559
|
-
});
|
|
2049
|
+
});
|