@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,13 +1,14 @@
|
|
|
1
|
-
import { respecfully, iReckon,
|
|
1
|
+
import { respecfully, iReckon, firstOfAll, firstOfEach, ifWeMight } from '@ibgib/helper-gib/dist/respec-gib/respec-gib.mjs';
|
|
2
2
|
const maam = `[${import.meta.url}]`, sir = maam;
|
|
3
3
|
import { clone, hash, getUUID } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
|
|
4
4
|
import { getIbGibAddr } from '@ibgib/ts-gib/dist/helper.mjs';
|
|
5
5
|
import { GLOBAL_LOG_A_LOT } from '../core-constants.mjs';
|
|
6
6
|
import { KeystoneStrategyFactory } from './strategy/keystone-strategy-factory.mjs';
|
|
7
|
+
import { KeystoneReplenishStrategy, KeystoneClaimType } from './keystone-types.mjs';
|
|
7
8
|
import { createRevocationPoolConfig, createStandardPoolConfig } from './keystone-config-builder.mjs';
|
|
8
|
-
import { POOL_ID_DEFAULT, POOL_ID_REVOKE, KEYSTONE_VERB_REVOKE, KEYSTONE_VERB_MANAGE, POOL_ID_MANAGE, KEYSTONE_VERB_SYNC } from './keystone-constants.mjs';
|
|
9
|
+
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';
|
|
9
10
|
import { KeystoneService_V1, } from './keystone-service-v1.mjs';
|
|
10
|
-
import { selectChallengeIds, validateChallengePool, validateKeystoneIb, validateKeystoneTransition, validateGenesisKeystone } from './keystone-helpers.mjs';
|
|
11
|
+
import { selectChallengeIds, validateChallengePool, validateKeystoneIb, validateKeystoneTransition, validateGenesisKeystone, rotatePoolChallenges } from './keystone-helpers.mjs';
|
|
11
12
|
import { KeystoneProfileBuilder } from './policy/keystone-profile-builder.mjs';
|
|
12
13
|
import { getTjpAddr } from '../common/other/ibgib-helper.mjs';
|
|
13
14
|
import { deriveDelegateSecret } from './strategy/hash-reveal-v1/hash-reveal-v1.mjs';
|
|
@@ -160,20 +161,20 @@ await respecfully(sir, 'Suite A: Strategy Vectors (HashRevealV1)', async () => {
|
|
|
160
161
|
});
|
|
161
162
|
});
|
|
162
163
|
await respecfully(sir, 'Derivation Logic', async () => {
|
|
163
|
-
await
|
|
164
|
+
await ifWeMight(sir, 'derivePoolSecret with same inputs returns same output', async () => {
|
|
164
165
|
const strategy = KeystoneStrategyFactory.create({ config });
|
|
165
166
|
const secretA = await strategy.derivePoolSecret({ masterSecret });
|
|
166
167
|
const secretB = await strategy.derivePoolSecret({ masterSecret });
|
|
167
168
|
iReckon(sir, secretA).asTo('secret consistency').willEqual(secretB);
|
|
168
169
|
iReckon(sir, secretA).asTo('secret length').isGonnaBeTruthy();
|
|
169
170
|
});
|
|
170
|
-
await
|
|
171
|
+
await ifWeMight(sir, 'derivePoolSecret with different master secret returns different output', async () => {
|
|
171
172
|
const strategy = KeystoneStrategyFactory.create({ config });
|
|
172
173
|
const secretA = await strategy.derivePoolSecret({ masterSecret });
|
|
173
174
|
const secretB = await strategy.derivePoolSecret({ masterSecret: masterSecret + "_diff" });
|
|
174
175
|
iReckon(sir, secretA).asTo('secrets differ').not.willEqual(secretB);
|
|
175
176
|
});
|
|
176
|
-
await
|
|
177
|
+
await ifWeMight(sir, 'derivePoolSecret with different salt returns different output', async () => {
|
|
177
178
|
// Modify salt in a copy of config
|
|
178
179
|
const configB = { ...config, salt: "OtherPool" };
|
|
179
180
|
const strategyA = KeystoneStrategyFactory.create({ config });
|
|
@@ -182,7 +183,7 @@ await respecfully(sir, 'Suite A: Strategy Vectors (HashRevealV1)', async () => {
|
|
|
182
183
|
const secretB = await strategyB.derivePoolSecret({ masterSecret });
|
|
183
184
|
iReckon(sir, secretA).asTo('salt affects secret').not.willEqual(secretB);
|
|
184
185
|
});
|
|
185
|
-
await
|
|
186
|
+
await ifWeMight(sir, 'deriveDelegateSecret derives a consistent secret using KDF', async () => {
|
|
186
187
|
const secretA = await deriveDelegateSecret({ masterSecret });
|
|
187
188
|
const secretB = await deriveDelegateSecret({ masterSecret });
|
|
188
189
|
iReckon(sir, secretA).asTo('consistency').willEqual(secretB);
|
|
@@ -192,7 +193,7 @@ await respecfully(sir, 'Suite A: Strategy Vectors (HashRevealV1)', async () => {
|
|
|
192
193
|
});
|
|
193
194
|
});
|
|
194
195
|
await respecfully(sir, 'Challenge/Solution Logic', async () => {
|
|
195
|
-
await
|
|
196
|
+
await ifWeMight(sir, 'generateSolution -> generateChallenge -> validateSolution loop works', async () => {
|
|
196
197
|
const strategy = KeystoneStrategyFactory.create({ config });
|
|
197
198
|
const poolSecret = await strategy.derivePoolSecret({ masterSecret });
|
|
198
199
|
const challengeId = "a3ff7843552870fc28bef2b"; // arbitrary random challengeId
|
|
@@ -207,7 +208,7 @@ await respecfully(sir, 'Suite A: Strategy Vectors (HashRevealV1)', async () => {
|
|
|
207
208
|
const isValid = await strategy.validateSolution({ solution, challenge });
|
|
208
209
|
iReckon(sir, isValid).asTo('valid pair should pass').isGonnaBeTrue();
|
|
209
210
|
});
|
|
210
|
-
await
|
|
211
|
+
await ifWeMight(sir, 'validateSolution fails for mismatched values', async () => {
|
|
211
212
|
const strategy = KeystoneStrategyFactory.create({ config });
|
|
212
213
|
const poolSecret = await strategy.derivePoolSecret({ masterSecret });
|
|
213
214
|
const challengeId = "8c994f3ed598f150e25513"; // arbitrary random challengeId
|
|
@@ -219,7 +220,7 @@ await respecfully(sir, 'Suite A: Strategy Vectors (HashRevealV1)', async () => {
|
|
|
219
220
|
const isValid = await strategy.validateSolution({ solution: badSolution, challenge });
|
|
220
221
|
iReckon(sir, isValid).asTo('tampered solution should fail').isGonnaBeFalse();
|
|
221
222
|
});
|
|
222
|
-
await
|
|
223
|
+
await ifWeMight(sir, 'validateSolution fails for mismatched challenge hashes', async () => {
|
|
223
224
|
const strategy = KeystoneStrategyFactory.create({ config });
|
|
224
225
|
const poolSecret = await strategy.derivePoolSecret({ masterSecret });
|
|
225
226
|
// Generate pair A
|
|
@@ -250,7 +251,7 @@ await respecfully(sir, 'Suite B: Service Lifecycle', async () => {
|
|
|
250
251
|
mockMetaspace = new MockMetaspaceService(mockSpace);
|
|
251
252
|
});
|
|
252
253
|
await respecfully(sir, 'Genesis', async () => {
|
|
253
|
-
await
|
|
254
|
+
await ifWeMight(sir, 'creates a valid genesis frame and persists it', async () => {
|
|
254
255
|
const salt = (await getUUID()).substring(0, 16);
|
|
255
256
|
const config = createStandardPoolConfig({
|
|
256
257
|
id: POOL_ID_DEFAULT,
|
|
@@ -276,7 +277,7 @@ await respecfully(sir, 'Suite B: Service Lifecycle', async () => {
|
|
|
276
277
|
});
|
|
277
278
|
});
|
|
278
279
|
await respecfully(sir, 'Signing (Evolution)', async () => {
|
|
279
|
-
await
|
|
280
|
+
await ifWeMight(sir, 'evolves the keystone with a valid proof', async () => {
|
|
280
281
|
const claim = {
|
|
281
282
|
target: "comment 123^gib",
|
|
282
283
|
verb: "post"
|
|
@@ -301,7 +302,7 @@ await respecfully(sir, 'Suite B: Service Lifecycle', async () => {
|
|
|
301
302
|
});
|
|
302
303
|
});
|
|
303
304
|
await respecfully(sir, 'Validation', async () => {
|
|
304
|
-
await
|
|
305
|
+
await ifWeMight(sir, 'validates the genesis->signed transition', async () => {
|
|
305
306
|
const errors = await service.validate({
|
|
306
307
|
prevIbGib: genesisKeystone,
|
|
307
308
|
currentIbGib: signedKeystone,
|
|
@@ -342,7 +343,7 @@ await respecfully(sir, 'Suite C: Security Vectors', async () => {
|
|
|
342
343
|
});
|
|
343
344
|
});
|
|
344
345
|
await respecfully(sir, 'Wrong Secret (Forgery)', async () => {
|
|
345
|
-
await
|
|
346
|
+
await ifWeMight(sir, 'prevents creation of forged frames', async () => {
|
|
346
347
|
const claim = { target: "comment 123^gib", verb: "post" };
|
|
347
348
|
let errorCaught = false;
|
|
348
349
|
let errorMsg = "";
|
|
@@ -368,7 +369,7 @@ await respecfully(sir, 'Suite C: Security Vectors', async () => {
|
|
|
368
369
|
});
|
|
369
370
|
});
|
|
370
371
|
await respecfully(sir, 'Policy Violation (Restricted Verbs)', async () => {
|
|
371
|
-
await
|
|
372
|
+
await ifWeMight(sir, 'throws error if signing forbidden verb with restricted pool', async () => {
|
|
372
373
|
// Create a specific restricted pool config manually
|
|
373
374
|
const restrictedPoolId = "read_only_pool";
|
|
374
375
|
const restrictedConfig = createStandardPoolConfig({
|
|
@@ -436,7 +437,7 @@ await respecfully(sir, 'Suite D: Revocation', async () => {
|
|
|
436
437
|
});
|
|
437
438
|
await respecfully(sir, 'Revoke Lifecycle', async () => {
|
|
438
439
|
let revokedKeystone;
|
|
439
|
-
await
|
|
440
|
+
await ifWeMight(sir, 'successfully creates a revocation frame', async () => {
|
|
440
441
|
revokedKeystone = await service.revoke({
|
|
441
442
|
latestKeystone: genesisKeystone,
|
|
442
443
|
masterSecret,
|
|
@@ -451,7 +452,7 @@ await respecfully(sir, 'Suite D: Revocation', async () => {
|
|
|
451
452
|
iReckon(sir, data.revocationInfo.reason).willEqual("Key compromised");
|
|
452
453
|
iReckon(sir, data.revocationInfo.proof.claim.verb).willEqual(KEYSTONE_VERB_REVOKE);
|
|
453
454
|
});
|
|
454
|
-
await
|
|
455
|
+
await ifWeMight(sir, 'validates the revocation frame', async () => {
|
|
455
456
|
const errors = await service.validate({
|
|
456
457
|
prevIbGib: genesisKeystone,
|
|
457
458
|
currentIbGib: revokedKeystone,
|
|
@@ -460,7 +461,7 @@ await respecfully(sir, 'Suite D: Revocation', async () => {
|
|
|
460
461
|
});
|
|
461
462
|
iReckon(sir, errors.length).asTo('no validation errors').willEqual(0);
|
|
462
463
|
});
|
|
463
|
-
await
|
|
464
|
+
await ifWeMight(sir, 'consumed the revocation pool (Scorched Earth)', async () => {
|
|
464
465
|
const data = revokedKeystone.data;
|
|
465
466
|
const revokePool = data.challengePools.find(p => p.id === POOL_ID_REVOKE);
|
|
466
467
|
// The pool should exist...
|
|
@@ -469,6 +470,42 @@ await respecfully(sir, 'Suite D: Revocation', async () => {
|
|
|
469
470
|
const remaining = Object.keys(revokePool.challenges);
|
|
470
471
|
iReckon(sir, remaining.length).asTo('pool depleted').willEqual(0);
|
|
471
472
|
});
|
|
473
|
+
await ifWeMight(sir, 'fails to sign any new claims after revocation', async () => {
|
|
474
|
+
let errorCaught = false;
|
|
475
|
+
try {
|
|
476
|
+
await service.sign({
|
|
477
|
+
latestKeystone: revokedKeystone,
|
|
478
|
+
masterSecret,
|
|
479
|
+
claim: { verb: "post", target: "some_target" },
|
|
480
|
+
metaspace: mockMetaspace,
|
|
481
|
+
space: mockSpace,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
catch (err) {
|
|
485
|
+
errorCaught = true;
|
|
486
|
+
iReckon(sir, err.message).includes("has been revoked");
|
|
487
|
+
}
|
|
488
|
+
iReckon(sir, errorCaught).asTo('signing blocked after revocation').isGonnaBeTrue();
|
|
489
|
+
});
|
|
490
|
+
await ifWeMight(sir, 'fails to evolve or add pools after revocation', async () => {
|
|
491
|
+
let errorCaught = false;
|
|
492
|
+
try {
|
|
493
|
+
const dummyConfig = createStandardPoolConfig({ id: "dummy", salt: "dummy" });
|
|
494
|
+
const dummyPool = { id: "dummy", config: dummyConfig, challenges: {} };
|
|
495
|
+
await service.addPools({
|
|
496
|
+
latestKeystone: revokedKeystone,
|
|
497
|
+
masterSecret,
|
|
498
|
+
newPools: [dummyPool],
|
|
499
|
+
metaspace: mockMetaspace,
|
|
500
|
+
space: mockSpace,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
catch (err) {
|
|
504
|
+
errorCaught = true;
|
|
505
|
+
iReckon(sir, err.message).includes("has been revoked");
|
|
506
|
+
}
|
|
507
|
+
iReckon(sir, errorCaught).asTo('adding pools blocked after revocation').isGonnaBeTrue();
|
|
508
|
+
});
|
|
472
509
|
});
|
|
473
510
|
});
|
|
474
511
|
// ===========================================================================
|
|
@@ -522,7 +559,7 @@ await respecfully(sir, 'Suite E: Structural Evolution (addPools)', async () => {
|
|
|
522
559
|
});
|
|
523
560
|
});
|
|
524
561
|
await respecfully(sir, 'Happy Path', async () => {
|
|
525
|
-
await
|
|
562
|
+
await ifWeMight(sir, 'authorizes and adds a foreign pool', async () => {
|
|
526
563
|
const bobPool = await createForeignPool("pool_bob", ["post"]);
|
|
527
564
|
const updatedKeystone = await service.addPools({
|
|
528
565
|
latestKeystone: aliceKeystone,
|
|
@@ -554,7 +591,7 @@ await respecfully(sir, 'Suite E: Structural Evolution (addPools)', async () => {
|
|
|
554
591
|
});
|
|
555
592
|
});
|
|
556
593
|
await respecfully(sir, 'Permissions & Logic', async () => {
|
|
557
|
-
await
|
|
594
|
+
await ifWeMight(sir, 'fails if no pool allows "manage" verb', async () => {
|
|
558
595
|
// 1. Create a restricted keystone
|
|
559
596
|
let id = "read_only";
|
|
560
597
|
const restrictedConfig = createStandardPoolConfig({ id, salt: id });
|
|
@@ -583,7 +620,7 @@ await respecfully(sir, 'Suite E: Structural Evolution (addPools)', async () => {
|
|
|
583
620
|
}
|
|
584
621
|
iReckon(sir, errorCaught).asTo('permission denied').isGonnaBeTrue();
|
|
585
622
|
});
|
|
586
|
-
await
|
|
623
|
+
await ifWeMight(sir, 'fails on ID collision', async () => {
|
|
587
624
|
// Try to add "pool_bob" again (it was added in Happy Path)
|
|
588
625
|
const duplicatePool = await createForeignPool("pool_bob");
|
|
589
626
|
let errorCaught = false;
|
|
@@ -658,7 +695,7 @@ await respecfully(sir, 'Suite E: Structural Evolution (addPools)', async () => {
|
|
|
658
695
|
});
|
|
659
696
|
});
|
|
660
697
|
await respecfully(sir, 'Happy Path', async () => {
|
|
661
|
-
await
|
|
698
|
+
await ifWeMight(sir, 'authorizes and adds a foreign pool', async () => {
|
|
662
699
|
const bobPool = await createForeignPool("pool_bob", ["post"]);
|
|
663
700
|
const updatedKeystone = await service.addPools({
|
|
664
701
|
latestKeystone: aliceKeystone,
|
|
@@ -690,7 +727,7 @@ await respecfully(sir, 'Suite E: Structural Evolution (addPools)', async () => {
|
|
|
690
727
|
});
|
|
691
728
|
});
|
|
692
729
|
await respecfully(sir, 'Permissions & Logic', async () => {
|
|
693
|
-
await
|
|
730
|
+
await ifWeMight(sir, 'fails if no pool allows "manage" verb', async () => {
|
|
694
731
|
// 1. Create a restricted keystone (read-only)
|
|
695
732
|
let id = "read_only";
|
|
696
733
|
const restrictedConfig = createStandardPoolConfig({ id, salt: id });
|
|
@@ -719,7 +756,7 @@ await respecfully(sir, 'Suite E: Structural Evolution (addPools)', async () => {
|
|
|
719
756
|
}
|
|
720
757
|
iReckon(sir, errorCaught).asTo('permission denied').isGonnaBeTrue();
|
|
721
758
|
});
|
|
722
|
-
await
|
|
759
|
+
await ifWeMight(sir, 'fails on ID collision', async () => {
|
|
723
760
|
// Try to add "pool_bob" again (it was added in Happy Path)
|
|
724
761
|
const duplicatePool = await createForeignPool("pool_bob");
|
|
725
762
|
let errorCaught = false;
|
|
@@ -771,7 +808,7 @@ await respecfully(sir, 'Suite F: Deep Inspection', async () => {
|
|
|
771
808
|
});
|
|
772
809
|
});
|
|
773
810
|
await respecfully(sir, 'Proof Granularity & Math', async () => {
|
|
774
|
-
await
|
|
811
|
+
await ifWeMight(sir, 'generates exactly the expected number of solutions', async () => {
|
|
775
812
|
signedKeystone = await service.sign({
|
|
776
813
|
latestKeystone: genesisKeystone,
|
|
777
814
|
masterSecret: aliceSecret,
|
|
@@ -785,7 +822,7 @@ await respecfully(sir, 'Suite F: Deep Inspection', async () => {
|
|
|
785
822
|
// 2 Sequential + 2 Random = 4
|
|
786
823
|
iReckon(sir, solutions.length).asTo('solution count').willEqual(4);
|
|
787
824
|
});
|
|
788
|
-
await
|
|
825
|
+
await ifWeMight(sir, 'verifies the math manually (White-box Crypto Check)', async () => {
|
|
789
826
|
const proof = signedKeystone.data.proofs[0];
|
|
790
827
|
const poolSnapshot = genesisKeystone.data.challengePools.find(p => p.id === poolId);
|
|
791
828
|
// We iterate every solution in the proof and MANUALLY verify the hash relationship
|
|
@@ -808,7 +845,7 @@ await respecfully(sir, 'Suite F: Deep Inspection', async () => {
|
|
|
808
845
|
iReckon(sir, calculatedHash).asTo(`Manual hash verification for ${solution.challengeId}`).willEqual(challenge.hash);
|
|
809
846
|
}
|
|
810
847
|
});
|
|
811
|
-
await
|
|
848
|
+
await ifWeMight(sir, 'verifies FIFO logic (Deterministic Selection)', async () => {
|
|
812
849
|
const proof = signedKeystone.data.proofs[0];
|
|
813
850
|
const poolSnapshot = genesisKeystone.data.challengePools.find(p => p.id === poolId);
|
|
814
851
|
// The first N keys in the pool should be the FIFO targets.
|
|
@@ -822,7 +859,7 @@ await respecfully(sir, 'Suite F: Deep Inspection', async () => {
|
|
|
822
859
|
iReckon(sir, hasFirst).asTo(`Solution includes 1st FIFO ID (${expectedFifoIds[0]})`).isGonnaBeTrue();
|
|
823
860
|
iReckon(sir, hasSecond).asTo(`Solution includes 2nd FIFO ID (${expectedFifoIds[1]})`).isGonnaBeTrue();
|
|
824
861
|
});
|
|
825
|
-
await
|
|
862
|
+
await ifWeMight(sir, 'verifies Next-Gen Hash-Based Target Binding Selection', async () => {
|
|
826
863
|
const bindingConfig = createStandardPoolConfig({
|
|
827
864
|
id: "binding_pool",
|
|
828
865
|
salt: "binding_salt",
|
|
@@ -864,7 +901,7 @@ await respecfully(sir, 'Suite F: Deep Inspection', async () => {
|
|
|
864
901
|
iReckon(sir, proofIds.includes(id)).asTo(`proof includes selected ID ${id}`).isGonnaBeTrue();
|
|
865
902
|
}
|
|
866
903
|
});
|
|
867
|
-
await
|
|
904
|
+
await ifWeMight(sir, 'fails validation if total requested challenges >= size', async () => {
|
|
868
905
|
const invalidConfig = createStandardPoolConfig({
|
|
869
906
|
id: "invalid_pool",
|
|
870
907
|
salt: "invalid_salt",
|
|
@@ -880,7 +917,7 @@ await respecfully(sir, 'Suite F: Deep Inspection', async () => {
|
|
|
880
917
|
});
|
|
881
918
|
});
|
|
882
919
|
await respecfully(sir, 'DTO & Serialization', async () => {
|
|
883
|
-
await
|
|
920
|
+
await ifWeMight(sir, 'survives a clone/JSON-cycle without corruption', async () => {
|
|
884
921
|
// 1. Create a DTO (simulate network transmission/storage)
|
|
885
922
|
// 'clone' does a JSON stringify/parse under the hood (usually) or structured clone.
|
|
886
923
|
const dto = clone(signedKeystone);
|
|
@@ -896,7 +933,7 @@ await respecfully(sir, 'Suite F: Deep Inspection', async () => {
|
|
|
896
933
|
});
|
|
897
934
|
iReckon(sir, errors.length).asTo('DTO validation errors').willEqual(0);
|
|
898
935
|
});
|
|
899
|
-
await
|
|
936
|
+
await ifWeMight(sir, 'ensures data contains no functions or circular refs', async () => {
|
|
900
937
|
// A crude but effective test: ensure JSON.stringify doesn't throw
|
|
901
938
|
// and the result is equal to the object (if we parsed it back).
|
|
902
939
|
const jsonStr = JSON.stringify(signedKeystone);
|
|
@@ -915,7 +952,7 @@ await respecfully(sir, 'Suite F: Deep Inspection', async () => {
|
|
|
915
952
|
});
|
|
916
953
|
});
|
|
917
954
|
await respecfully(sir, 'KeystoneProfileBuilder and Modular Schemas', async () => {
|
|
918
|
-
await
|
|
955
|
+
await ifWeMight(sir, 'compiles configs successfully with medium profile', async () => {
|
|
919
956
|
const builder = KeystoneProfileBuilder.buildKeystone('medium')
|
|
920
957
|
.withUsername('alice')
|
|
921
958
|
.withEmail('alice@example.com')
|
|
@@ -934,7 +971,17 @@ await respecfully(sir, 'Suite F: Deep Inspection', async () => {
|
|
|
934
971
|
iReckon(sir, details.description).asTo('description').willEqual('Alice test keystone');
|
|
935
972
|
iReckon(sir, details.extraInfo).asTo('extraInfo').willEqual('abc');
|
|
936
973
|
});
|
|
937
|
-
await
|
|
974
|
+
await ifWeMight(sir, 'compiles configs successfully with domain profile (omitting connect)', async () => {
|
|
975
|
+
const builder = KeystoneProfileBuilder.buildKeystone('domain')
|
|
976
|
+
.withUsername('alice')
|
|
977
|
+
.withEmail('alice@example.com')
|
|
978
|
+
.withDescription('Alice domain keystone');
|
|
979
|
+
const configs = await builder.compileConfigs();
|
|
980
|
+
iReckon(sir, configs.length).asTo('number of pools').willEqual(4);
|
|
981
|
+
const connectPool = configs.find(c => c.id === 'connect');
|
|
982
|
+
iReckon(sir, connectPool).asTo('connect pool omitted').isGonnaBeUndefined();
|
|
983
|
+
});
|
|
984
|
+
await ifWeMight(sir, 'supports customized overrides', async () => {
|
|
938
985
|
const builder = KeystoneProfileBuilder.buildKeystone('test')
|
|
939
986
|
.customizePool('sync', {
|
|
940
987
|
behavior: {
|
|
@@ -949,7 +996,7 @@ await respecfully(sir, 'Suite F: Deep Inspection', async () => {
|
|
|
949
996
|
});
|
|
950
997
|
});
|
|
951
998
|
await respecfully(sir, 'Keystone Metadata Validation', async () => {
|
|
952
|
-
await
|
|
999
|
+
await ifWeMight(sir, 'validates correct username and description', async () => {
|
|
953
1000
|
const service = new KeystoneService_V1();
|
|
954
1001
|
const space = new MockIbGibSpace();
|
|
955
1002
|
const metaspace = new MockMetaspaceService(space);
|
|
@@ -970,7 +1017,7 @@ await respecfully(sir, 'Suite F: Deep Inspection', async () => {
|
|
|
970
1017
|
const errors = await service.validateGenesisKeystone({ keystoneIbGib });
|
|
971
1018
|
iReckon(sir, errors.length).asTo('errors count').willEqual(0);
|
|
972
1019
|
});
|
|
973
|
-
await
|
|
1020
|
+
await ifWeMight(sir, 'fails validation for invalid username length or character', async () => {
|
|
974
1021
|
const service = new KeystoneService_V1();
|
|
975
1022
|
const space = new MockIbGibSpace();
|
|
976
1023
|
const metaspace = new MockMetaspaceService(space);
|
|
@@ -990,7 +1037,7 @@ await respecfully(sir, 'Suite F: Deep Inspection', async () => {
|
|
|
990
1037
|
iReckon(sir, errors.length).asTo('errors count').willEqual(1);
|
|
991
1038
|
iReckon(sir, errors[0]).includes('invalid username');
|
|
992
1039
|
});
|
|
993
|
-
await
|
|
1040
|
+
await ifWeMight(sir, 'fails validation for invalid description characters', async () => {
|
|
994
1041
|
const service = new KeystoneService_V1();
|
|
995
1042
|
const space = new MockIbGibSpace();
|
|
996
1043
|
const metaspace = new MockMetaspaceService(space);
|
|
@@ -1047,7 +1094,7 @@ await respecfully(sir, 'Suite E: Delegation (Register/Unregister)', async () =>
|
|
|
1047
1094
|
space: mockSpace,
|
|
1048
1095
|
});
|
|
1049
1096
|
});
|
|
1050
|
-
await
|
|
1097
|
+
await ifWeMight(sir, 'registers a delegate keystone and evolves parent using manage pool', async () => {
|
|
1051
1098
|
// Register the delegate
|
|
1052
1099
|
const evolvedParent = await service.registerDelegate({
|
|
1053
1100
|
parentKeystone,
|
|
@@ -1067,7 +1114,7 @@ await respecfully(sir, 'Suite E: Delegation (Register/Unregister)', async () =>
|
|
|
1067
1114
|
iReckon(sir, delegateInfo).asTo('delegate info registered').isGonnaBeTruthy();
|
|
1068
1115
|
iReckon(sir, delegateInfo?.delegateTjpAddr).asTo('delegate tjp matches').willEqual(delegateTjpAddr);
|
|
1069
1116
|
});
|
|
1070
|
-
await
|
|
1117
|
+
await ifWeMight(sir, 'gets correct delegate status via parent TJP address', async () => {
|
|
1071
1118
|
const parentTjpAddr = getTjpAddr({ ibGib: parentKeystone });
|
|
1072
1119
|
if (!parentTjpAddr) {
|
|
1073
1120
|
throw new Error(`(UNEXPECTED) parentTjpAddr falsy? (E: b7ce2835ccd951d8c99257328be3b826)`);
|
|
@@ -1103,7 +1150,7 @@ await respecfully(sir, 'Suite E: Delegation (Register/Unregister)', async () =>
|
|
|
1103
1150
|
iReckon(sir, statusAfter.registered).asTo('registered status').isGonnaBeTrue();
|
|
1104
1151
|
iReckon(sir, statusAfter.delegateInfo?.delegateTjpAddr).asTo('status tjp matches').willEqual(delegateTjpAddr);
|
|
1105
1152
|
});
|
|
1106
|
-
await
|
|
1153
|
+
await ifWeMight(sir, 'unregisters a registered delegate keystone', async () => {
|
|
1107
1154
|
// Register first
|
|
1108
1155
|
const evolvedParent = await service.registerDelegate({
|
|
1109
1156
|
parentKeystone,
|
|
@@ -1129,7 +1176,7 @@ await respecfully(sir, 'Suite E: Delegation (Register/Unregister)', async () =>
|
|
|
1129
1176
|
const info = finalParent.data?.delegates?.[delegateTjpAddr];
|
|
1130
1177
|
iReckon(sir, info).asTo('delegate info removed').isGonnaBeUndefined();
|
|
1131
1178
|
});
|
|
1132
|
-
await
|
|
1179
|
+
await ifWeMight(sir, 'throws if parent does not have a manage pool', async () => {
|
|
1133
1180
|
// Create a parent WITHOUT a manage pool
|
|
1134
1181
|
const salt = (await getUUID()).substring(0, 16);
|
|
1135
1182
|
const defaultConfig = createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt });
|
|
@@ -1189,13 +1236,13 @@ await respecfully(sir, 'Suite G: Delegate & IB Validation', async () => {
|
|
|
1189
1236
|
space: mockSpace,
|
|
1190
1237
|
});
|
|
1191
1238
|
});
|
|
1192
|
-
await
|
|
1239
|
+
await ifWeMight(sir, 'validateKeystoneIb returns true for valid keystone ib and false for others', async () => {
|
|
1193
1240
|
iReckon(sir, await validateKeystoneIb({ ib: 'keystone' })).isGonnaBeTrue();
|
|
1194
1241
|
iReckon(sir, await validateKeystoneIb({ ib: 'keystone user info' })).isGonnaBeTrue();
|
|
1195
1242
|
iReckon(sir, await validateKeystoneIb({ ib: 'invalidAtom' })).isGonnaBeFalse();
|
|
1196
1243
|
iReckon(sir, await validateKeystoneIb({ ib: 'not_keystone sub_atom' })).isGonnaBeFalse();
|
|
1197
1244
|
});
|
|
1198
|
-
await
|
|
1245
|
+
await ifWeMight(sir, 'validateGenesisKeystone fails validation if ib is invalid', async () => {
|
|
1199
1246
|
const invalidGenesis = {
|
|
1200
1247
|
...parentKeystone,
|
|
1201
1248
|
ib: 'not_a_keystone^' + parentKeystone.gib,
|
|
@@ -1204,7 +1251,7 @@ await respecfully(sir, 'Suite G: Delegate & IB Validation', async () => {
|
|
|
1204
1251
|
iReckon(sir, errors.length > 0).isGonnaBeTrue();
|
|
1205
1252
|
iReckon(sir, errors[0]).includes('invalid keystone ib');
|
|
1206
1253
|
});
|
|
1207
|
-
await
|
|
1254
|
+
await ifWeMight(sir, 'validateKeystoneTransition passes for valid delegate registration', async () => {
|
|
1208
1255
|
const evolvedParent = await service.registerDelegate({
|
|
1209
1256
|
parentKeystone,
|
|
1210
1257
|
delegateKeystone,
|
|
@@ -1219,7 +1266,7 @@ await respecfully(sir, 'Suite G: Delegate & IB Validation', async () => {
|
|
|
1219
1266
|
});
|
|
1220
1267
|
iReckon(sir, errors.length).willEqual(0);
|
|
1221
1268
|
});
|
|
1222
|
-
await
|
|
1269
|
+
await ifWeMight(sir, 'validateKeystoneTransition fails if delegates map is mutated without manage proof', async () => {
|
|
1223
1270
|
const evolvedParent = await service.registerDelegate({
|
|
1224
1271
|
parentKeystone,
|
|
1225
1272
|
delegateKeystone,
|
|
@@ -1242,7 +1289,7 @@ await respecfully(sir, 'Suite G: Delegate & IB Validation', async () => {
|
|
|
1242
1289
|
iReckon(sir, errors.length > 0).isGonnaBeTrue();
|
|
1243
1290
|
iReckon(sir, errors.some(e => e.includes('Policy Violation: Delegates map was mutated'))).isGonnaBeTrue();
|
|
1244
1291
|
});
|
|
1245
|
-
await
|
|
1292
|
+
await ifWeMight(sir, 'validateKeystoneTransition fails if delegate key does not match delegateTjpAddr', async () => {
|
|
1246
1293
|
const evolvedParent = await service.registerDelegate({
|
|
1247
1294
|
parentKeystone,
|
|
1248
1295
|
delegateKeystone,
|
|
@@ -1266,7 +1313,7 @@ await respecfully(sir, 'Suite G: Delegate & IB Validation', async () => {
|
|
|
1266
1313
|
iReckon(sir, errors.length > 0).isGonnaBeTrue();
|
|
1267
1314
|
iReckon(sir, errors.some(e => e.includes('does not match delegateTjpAddr'))).isGonnaBeTrue();
|
|
1268
1315
|
});
|
|
1269
|
-
await
|
|
1316
|
+
await ifWeMight(sir, 'validateKeystoneTransition fails if delegate addresses have invalid keystone ib', async () => {
|
|
1270
1317
|
const evolvedParent = await service.registerDelegate({
|
|
1271
1318
|
parentKeystone,
|
|
1272
1319
|
delegateKeystone,
|
|
@@ -1288,7 +1335,7 @@ await respecfully(sir, 'Suite G: Delegate & IB Validation', async () => {
|
|
|
1288
1335
|
iReckon(sir, errors.length > 0).isGonnaBeTrue();
|
|
1289
1336
|
iReckon(sir, errors.some(e => e.includes('not a valid keystone address'))).isGonnaBeTrue();
|
|
1290
1337
|
});
|
|
1291
|
-
await
|
|
1338
|
+
await ifWeMight(sir, 'validateKeystoneTransition fails if thisAddr is not in the parent timeline history', async () => {
|
|
1292
1339
|
const evolvedParent = await service.registerDelegate({
|
|
1293
1340
|
parentKeystone,
|
|
1294
1341
|
delegateKeystone,
|
|
@@ -1311,4 +1358,384 @@ await respecfully(sir, 'Suite G: Delegate & IB Validation', async () => {
|
|
|
1311
1358
|
iReckon(sir, errors.some(e => e.includes('not a valid historical address in the parent keystone timeline'))).isGonnaBeTrue();
|
|
1312
1359
|
});
|
|
1313
1360
|
});
|
|
1361
|
+
// ===========================================================================
|
|
1362
|
+
// SUITE G: COMBINED POOLS & PASSWORD ROTATION
|
|
1363
|
+
// ===========================================================================
|
|
1364
|
+
await respecfully(sir, 'Suite G: Combined Pools & Password Rotation', async () => {
|
|
1365
|
+
const service = new KeystoneService_V1();
|
|
1366
|
+
const userSecret = "UserMasterSecret_123";
|
|
1367
|
+
const custodianSecret = "CustodianSecret_456";
|
|
1368
|
+
let mockSpace;
|
|
1369
|
+
let mockMetaspace;
|
|
1370
|
+
firstOfEach(sir, async () => {
|
|
1371
|
+
mockSpace = new MockIbGibSpace();
|
|
1372
|
+
mockMetaspace = new MockMetaspaceService(mockSpace);
|
|
1373
|
+
});
|
|
1374
|
+
await ifWeMight(sir, 'revokes a keystone using a separate revocation pool', async () => {
|
|
1375
|
+
const stdSalt = "std_salt";
|
|
1376
|
+
const revokeSalt = "revoke_salt";
|
|
1377
|
+
const stdConfig = createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt: stdSalt });
|
|
1378
|
+
const revokeConfig = createStandardPoolConfig({ id: POOL_ID_REVOKE, salt: revokeSalt });
|
|
1379
|
+
revokeConfig.allowedVerbs = [KEYSTONE_VERB_REVOKE];
|
|
1380
|
+
revokeConfig.behavior.replenish = KeystoneReplenishStrategy.deleteAll;
|
|
1381
|
+
const keystone = await service.genesis({
|
|
1382
|
+
masterSecret: userSecret,
|
|
1383
|
+
configs: [stdConfig, revokeConfig],
|
|
1384
|
+
metaspace: mockMetaspace,
|
|
1385
|
+
space: mockSpace,
|
|
1386
|
+
});
|
|
1387
|
+
const revoked = await service.revoke({
|
|
1388
|
+
latestKeystone: keystone,
|
|
1389
|
+
masterSecret: userSecret,
|
|
1390
|
+
metaspace: mockMetaspace,
|
|
1391
|
+
space: mockSpace,
|
|
1392
|
+
});
|
|
1393
|
+
iReckon(sir, revoked.data?.revocationInfo).asTo('revocationInfo present').isGonnaBeTruthy();
|
|
1394
|
+
const revokePool = revoked.data?.challengePools.find(p => p.id === POOL_ID_REVOKE);
|
|
1395
|
+
iReckon(sir, Object.keys(revokePool.challenges).length).asTo('revoke pool depleted').willEqual(0);
|
|
1396
|
+
});
|
|
1397
|
+
await ifWeMight(sir, 'revokes a keystone using a combined manage/revoke pool and empties it', async () => {
|
|
1398
|
+
const salt = "combined_salt";
|
|
1399
|
+
const manageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt });
|
|
1400
|
+
manageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
1401
|
+
const keystone = await service.genesis({
|
|
1402
|
+
masterSecret: userSecret,
|
|
1403
|
+
configs: [manageConfig],
|
|
1404
|
+
metaspace: mockMetaspace,
|
|
1405
|
+
space: mockSpace,
|
|
1406
|
+
});
|
|
1407
|
+
const revoked = await service.revoke({
|
|
1408
|
+
latestKeystone: keystone,
|
|
1409
|
+
masterSecret: userSecret,
|
|
1410
|
+
metaspace: mockMetaspace,
|
|
1411
|
+
space: mockSpace,
|
|
1412
|
+
});
|
|
1413
|
+
iReckon(sir, revoked.data?.revocationInfo).asTo('revocationInfo present').isGonnaBeTruthy();
|
|
1414
|
+
const managePool = revoked.data?.challengePools.find(p => p.id === POOL_ID_MANAGE);
|
|
1415
|
+
iReckon(sir, Object.keys(managePool.challenges).length).asTo('combined manage pool depleted').willEqual(0);
|
|
1416
|
+
});
|
|
1417
|
+
await ifWeMight(sir, 'rotates user password successfully when signed by the user manage pool', async () => {
|
|
1418
|
+
const salt = "manage_salt";
|
|
1419
|
+
const manageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt });
|
|
1420
|
+
manageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
1421
|
+
const keystone = await service.genesis({
|
|
1422
|
+
masterSecret: userSecret,
|
|
1423
|
+
configs: [manageConfig],
|
|
1424
|
+
metaspace: mockMetaspace,
|
|
1425
|
+
space: mockSpace,
|
|
1426
|
+
});
|
|
1427
|
+
const newSecret = "NewUserSecret_999";
|
|
1428
|
+
const rotated = await service.changePassword({
|
|
1429
|
+
latestKeystone: keystone,
|
|
1430
|
+
newMasterSecret: newSecret,
|
|
1431
|
+
signingSecret: userSecret,
|
|
1432
|
+
signingPoolId: POOL_ID_MANAGE,
|
|
1433
|
+
metaspace: mockMetaspace,
|
|
1434
|
+
space: mockSpace,
|
|
1435
|
+
});
|
|
1436
|
+
iReckon(sir, rotated).isGonnaBeTruthy();
|
|
1437
|
+
// 1. Try to sign using the new secret (should succeed)
|
|
1438
|
+
const claim = { verb: KEYSTONE_VERB_MANAGE, target: "test_target" };
|
|
1439
|
+
const signedNew = await service.sign({
|
|
1440
|
+
latestKeystone: rotated,
|
|
1441
|
+
masterSecret: newSecret,
|
|
1442
|
+
claim,
|
|
1443
|
+
poolId: POOL_ID_MANAGE,
|
|
1444
|
+
metaspace: mockMetaspace,
|
|
1445
|
+
space: mockSpace,
|
|
1446
|
+
});
|
|
1447
|
+
iReckon(sir, signedNew).isGonnaBeTruthy();
|
|
1448
|
+
// 2. Try to sign using the old secret (should fail)
|
|
1449
|
+
let errorCaught = false;
|
|
1450
|
+
try {
|
|
1451
|
+
await service.sign({
|
|
1452
|
+
latestKeystone: rotated,
|
|
1453
|
+
masterSecret: userSecret,
|
|
1454
|
+
claim,
|
|
1455
|
+
poolId: POOL_ID_MANAGE,
|
|
1456
|
+
metaspace: mockMetaspace,
|
|
1457
|
+
space: mockSpace,
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
catch {
|
|
1461
|
+
errorCaught = true;
|
|
1462
|
+
}
|
|
1463
|
+
iReckon(sir, errorCaught).asTo('old secret signing rejected').isGonnaBeTrue();
|
|
1464
|
+
});
|
|
1465
|
+
await ifWeMight(sir, 'recovers user password successfully when signed by the custodian manage pool', async () => {
|
|
1466
|
+
const userSalt = "user_salt";
|
|
1467
|
+
const custodianSalt = "custodian_salt";
|
|
1468
|
+
const userManageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: userSalt });
|
|
1469
|
+
userManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
1470
|
+
const custodianManageConfig = createStandardPoolConfig({ id: POOL_ID_CUSTODIAN_MANAGE, salt: custodianSalt });
|
|
1471
|
+
custodianManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
1472
|
+
// We simulate constructing the custodian pool "offline" by the server
|
|
1473
|
+
const strategy = KeystoneStrategyFactory.create({ config: custodianManageConfig });
|
|
1474
|
+
const poolSecret = await strategy.derivePoolSecret({ masterSecret: custodianSecret });
|
|
1475
|
+
const challenges = {};
|
|
1476
|
+
for (let i = 0; i < custodianManageConfig.behavior.size; i++) {
|
|
1477
|
+
const raw = await hash({ s: `${custodianManageConfig.salt}${Date.now()}${i}` });
|
|
1478
|
+
const challengeId = raw.substring(0, 16);
|
|
1479
|
+
const solution = await strategy.generateSolution({ poolSecret, poolId: custodianManageConfig.id, challengeId });
|
|
1480
|
+
challenges[challengeId] = await strategy.generateChallenge({ solution });
|
|
1481
|
+
}
|
|
1482
|
+
const custodianPool = {
|
|
1483
|
+
id: POOL_ID_CUSTODIAN_MANAGE,
|
|
1484
|
+
config: custodianManageConfig,
|
|
1485
|
+
challenges,
|
|
1486
|
+
isForeign: true
|
|
1487
|
+
};
|
|
1488
|
+
const keystone = await service.genesis({
|
|
1489
|
+
masterSecret: userSecret,
|
|
1490
|
+
configs: [userManageConfig],
|
|
1491
|
+
metaspace: mockMetaspace,
|
|
1492
|
+
space: mockSpace,
|
|
1493
|
+
});
|
|
1494
|
+
// Add the custodian pool to the keystone
|
|
1495
|
+
const keystoneWithCustodian = await service.addPools({
|
|
1496
|
+
latestKeystone: keystone,
|
|
1497
|
+
masterSecret: userSecret,
|
|
1498
|
+
newPools: [custodianPool],
|
|
1499
|
+
metaspace: mockMetaspace,
|
|
1500
|
+
space: mockSpace,
|
|
1501
|
+
});
|
|
1502
|
+
// The user forgets their secret. Recover using custodianSecret!
|
|
1503
|
+
const recoveredSecret = "RecoveredUserSecret_777";
|
|
1504
|
+
const recoveredKeystone = await service.changePassword({
|
|
1505
|
+
latestKeystone: keystoneWithCustodian,
|
|
1506
|
+
newMasterSecret: recoveredSecret,
|
|
1507
|
+
signingSecret: custodianSecret,
|
|
1508
|
+
signingPoolId: POOL_ID_CUSTODIAN_MANAGE,
|
|
1509
|
+
metaspace: mockMetaspace,
|
|
1510
|
+
space: mockSpace,
|
|
1511
|
+
});
|
|
1512
|
+
iReckon(sir, recoveredKeystone).isGonnaBeTruthy();
|
|
1513
|
+
// Verify we can sign with recovered secret
|
|
1514
|
+
const claim = { verb: KEYSTONE_VERB_MANAGE, target: "test_target" };
|
|
1515
|
+
const signedNew = await service.sign({
|
|
1516
|
+
latestKeystone: recoveredKeystone,
|
|
1517
|
+
masterSecret: recoveredSecret,
|
|
1518
|
+
claim,
|
|
1519
|
+
poolId: POOL_ID_MANAGE,
|
|
1520
|
+
metaspace: mockMetaspace,
|
|
1521
|
+
space: mockSpace,
|
|
1522
|
+
});
|
|
1523
|
+
iReckon(sir, signedNew).isGonnaBeTruthy();
|
|
1524
|
+
});
|
|
1525
|
+
await ifWeMight(sir, 'rotates custodian manage pool successfully', async () => {
|
|
1526
|
+
const userSalt = "user_salt";
|
|
1527
|
+
const custodianSalt = "custodian_salt";
|
|
1528
|
+
const userManageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: userSalt });
|
|
1529
|
+
userManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
1530
|
+
const custodianManageConfig = createStandardPoolConfig({ id: POOL_ID_CUSTODIAN_MANAGE, salt: custodianSalt });
|
|
1531
|
+
custodianManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
1532
|
+
// Custodian Pool genesis
|
|
1533
|
+
const strategy = KeystoneStrategyFactory.create({ config: custodianManageConfig });
|
|
1534
|
+
const poolSecret = await strategy.derivePoolSecret({ masterSecret: custodianSecret });
|
|
1535
|
+
const challenges = {};
|
|
1536
|
+
for (let i = 0; i < custodianManageConfig.behavior.size; i++) {
|
|
1537
|
+
const raw = await hash({ s: `${custodianManageConfig.salt}${Date.now()}${i}` });
|
|
1538
|
+
const challengeId = raw.substring(0, 16);
|
|
1539
|
+
const solution = await strategy.generateSolution({ poolSecret, poolId: custodianManageConfig.id, challengeId });
|
|
1540
|
+
challenges[challengeId] = await strategy.generateChallenge({ solution });
|
|
1541
|
+
}
|
|
1542
|
+
const custodianPool = {
|
|
1543
|
+
id: POOL_ID_CUSTODIAN_MANAGE,
|
|
1544
|
+
config: custodianManageConfig,
|
|
1545
|
+
challenges,
|
|
1546
|
+
isForeign: true
|
|
1547
|
+
};
|
|
1548
|
+
const keystone = await service.genesis({
|
|
1549
|
+
masterSecret: userSecret,
|
|
1550
|
+
configs: [userManageConfig],
|
|
1551
|
+
metaspace: mockMetaspace,
|
|
1552
|
+
space: mockSpace,
|
|
1553
|
+
});
|
|
1554
|
+
const keystoneWithCustodian = await service.addPools({
|
|
1555
|
+
latestKeystone: keystone,
|
|
1556
|
+
masterSecret: userSecret,
|
|
1557
|
+
newPools: [custodianPool],
|
|
1558
|
+
metaspace: mockMetaspace,
|
|
1559
|
+
space: mockSpace,
|
|
1560
|
+
});
|
|
1561
|
+
// Rotate custodian credentials to new secret
|
|
1562
|
+
const newCustodianSecret = "NewCustodianSecret_888";
|
|
1563
|
+
const newCustodianConfig = createStandardPoolConfig({ id: POOL_ID_CUSTODIAN_MANAGE, salt: "new_custodian_salt" });
|
|
1564
|
+
newCustodianConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
1565
|
+
const newStrategy = KeystoneStrategyFactory.create({ config: newCustodianConfig });
|
|
1566
|
+
const newPoolSecret = await newStrategy.derivePoolSecret({ masterSecret: newCustodianSecret });
|
|
1567
|
+
const newChallenges = {};
|
|
1568
|
+
for (let i = 0; i < newCustodianConfig.behavior.size; i++) {
|
|
1569
|
+
const raw = await hash({ s: `${newCustodianConfig.salt}${Date.now()}${i}` });
|
|
1570
|
+
const challengeId = raw.substring(0, 16);
|
|
1571
|
+
const solution = await newStrategy.generateSolution({ poolSecret: newPoolSecret, poolId: newCustodianConfig.id, challengeId });
|
|
1572
|
+
newChallenges[challengeId] = await newStrategy.generateChallenge({ solution });
|
|
1573
|
+
}
|
|
1574
|
+
const newCustodianPool = {
|
|
1575
|
+
id: POOL_ID_CUSTODIAN_MANAGE,
|
|
1576
|
+
config: newCustodianConfig,
|
|
1577
|
+
challenges: newChallenges,
|
|
1578
|
+
isForeign: true
|
|
1579
|
+
};
|
|
1580
|
+
// Rotate signed by user manage pool
|
|
1581
|
+
const rotated = await service.replacePool({
|
|
1582
|
+
latestKeystone: keystoneWithCustodian,
|
|
1583
|
+
newPool: newCustodianPool,
|
|
1584
|
+
signingSecret: userSecret,
|
|
1585
|
+
signingPoolId: POOL_ID_MANAGE,
|
|
1586
|
+
metaspace: mockMetaspace,
|
|
1587
|
+
space: mockSpace,
|
|
1588
|
+
});
|
|
1589
|
+
iReckon(sir, rotated).isGonnaBeTruthy();
|
|
1590
|
+
// Verify we can sign with new custodian secret
|
|
1591
|
+
const claim = { verb: KEYSTONE_VERB_MANAGE, target: "test_target" };
|
|
1592
|
+
const signedNew = await service.sign({
|
|
1593
|
+
latestKeystone: rotated,
|
|
1594
|
+
masterSecret: newCustodianSecret,
|
|
1595
|
+
claim,
|
|
1596
|
+
poolId: POOL_ID_CUSTODIAN_MANAGE,
|
|
1597
|
+
metaspace: mockMetaspace,
|
|
1598
|
+
space: mockSpace,
|
|
1599
|
+
});
|
|
1600
|
+
iReckon(sir, signedNew).isGonnaBeTruthy();
|
|
1601
|
+
});
|
|
1602
|
+
await ifWeMight(sir, 'rotates all non-foreign pools during changePassword by default', async () => {
|
|
1603
|
+
const userSalt = "user_salt";
|
|
1604
|
+
const custodianSalt = "custodian_salt";
|
|
1605
|
+
const secondarySalt = "secondary_salt";
|
|
1606
|
+
const userManageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: userSalt });
|
|
1607
|
+
userManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
1608
|
+
// Another native/non-foreign pool
|
|
1609
|
+
const secondaryNativeConfig = createStandardPoolConfig({ id: "secondary-native", salt: secondarySalt });
|
|
1610
|
+
secondaryNativeConfig.allowedVerbs = [KEYSTONE_VERB_SYNC];
|
|
1611
|
+
// A custodian/foreign pool
|
|
1612
|
+
const custodianManageConfig = createStandardPoolConfig({ id: POOL_ID_CUSTODIAN_MANAGE, salt: custodianSalt });
|
|
1613
|
+
custodianManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
1614
|
+
// Custodian Pool genesis
|
|
1615
|
+
const strategy = KeystoneStrategyFactory.create({ config: custodianManageConfig });
|
|
1616
|
+
const poolSecret = await strategy.derivePoolSecret({ masterSecret: custodianSecret });
|
|
1617
|
+
const challenges = {};
|
|
1618
|
+
for (let i = 0; i < custodianManageConfig.behavior.size; i++) {
|
|
1619
|
+
const raw = await hash({ s: `${custodianManageConfig.salt}${Date.now()}${i}` });
|
|
1620
|
+
const challengeId = raw.substring(0, 16);
|
|
1621
|
+
const solution = await strategy.generateSolution({ poolSecret, poolId: custodianManageConfig.id, challengeId });
|
|
1622
|
+
challenges[challengeId] = await strategy.generateChallenge({ solution });
|
|
1623
|
+
}
|
|
1624
|
+
const custodianPool = {
|
|
1625
|
+
id: POOL_ID_CUSTODIAN_MANAGE,
|
|
1626
|
+
config: custodianManageConfig,
|
|
1627
|
+
challenges,
|
|
1628
|
+
isForeign: true
|
|
1629
|
+
};
|
|
1630
|
+
const keystone = await service.genesis({
|
|
1631
|
+
masterSecret: userSecret,
|
|
1632
|
+
configs: [userManageConfig, secondaryNativeConfig],
|
|
1633
|
+
metaspace: mockMetaspace,
|
|
1634
|
+
space: mockSpace,
|
|
1635
|
+
});
|
|
1636
|
+
const keystoneWithCustodian = await service.addPools({
|
|
1637
|
+
latestKeystone: keystone,
|
|
1638
|
+
masterSecret: userSecret,
|
|
1639
|
+
newPools: [custodianPool],
|
|
1640
|
+
metaspace: mockMetaspace,
|
|
1641
|
+
space: mockSpace,
|
|
1642
|
+
});
|
|
1643
|
+
// Rotate passphrase/password
|
|
1644
|
+
const newPassphrase = "SuperNewSecretPassphrase_999";
|
|
1645
|
+
const rotated = await service.changePassword({
|
|
1646
|
+
latestKeystone: keystoneWithCustodian,
|
|
1647
|
+
newMasterSecret: newPassphrase,
|
|
1648
|
+
signingSecret: userSecret,
|
|
1649
|
+
signingPoolId: POOL_ID_MANAGE,
|
|
1650
|
+
metaspace: mockMetaspace,
|
|
1651
|
+
space: mockSpace,
|
|
1652
|
+
});
|
|
1653
|
+
iReckon(sir, rotated).isGonnaBeTruthy();
|
|
1654
|
+
const data = rotated.data;
|
|
1655
|
+
const rotatedManage = data.challengePools.find(p => p.id === POOL_ID_MANAGE);
|
|
1656
|
+
const rotatedSecondary = data.challengePools.find(p => p.id === "secondary-native");
|
|
1657
|
+
const rotatedCustodian = data.challengePools.find(p => p.id === POOL_ID_CUSTODIAN_MANAGE);
|
|
1658
|
+
// Verify native pools rotated (salt changed)
|
|
1659
|
+
iReckon(sir, rotatedManage.config.salt).not.willEqual(userSalt);
|
|
1660
|
+
iReckon(sir, rotatedSecondary.config.salt).not.willEqual(secondarySalt);
|
|
1661
|
+
// Verify foreign pool was not modified (salt identical)
|
|
1662
|
+
iReckon(sir, rotatedCustodian.config.salt).willEqual(custodianSalt);
|
|
1663
|
+
// Verify we can validate transition
|
|
1664
|
+
const errors = await service.validate({
|
|
1665
|
+
prevIbGib: keystoneWithCustodian,
|
|
1666
|
+
currentIbGib: rotated
|
|
1667
|
+
});
|
|
1668
|
+
iReckon(sir, errors.length).willEqual(0);
|
|
1669
|
+
});
|
|
1670
|
+
await ifWeMight(sir, 'fails validation if password rotation details or constraints are violated', async () => {
|
|
1671
|
+
// Construct invalid change-password transition
|
|
1672
|
+
const userManageConfig = createStandardPoolConfig({ id: POOL_ID_MANAGE, salt: "salt1" });
|
|
1673
|
+
userManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
1674
|
+
const custodianManageConfig = createStandardPoolConfig({ id: POOL_ID_CUSTODIAN_MANAGE, salt: "salt2" });
|
|
1675
|
+
custodianManageConfig.allowedVerbs = [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_REVOKE];
|
|
1676
|
+
// Custodian Pool genesis
|
|
1677
|
+
const strategy = KeystoneStrategyFactory.create({ config: custodianManageConfig });
|
|
1678
|
+
const poolSecret = await strategy.derivePoolSecret({ masterSecret: custodianSecret });
|
|
1679
|
+
const challenges = {};
|
|
1680
|
+
for (let i = 0; i < custodianManageConfig.behavior.size; i++) {
|
|
1681
|
+
const raw = await hash({ s: `${custodianManageConfig.salt}${Date.now()}${i}` });
|
|
1682
|
+
const challengeId = raw.substring(0, 16);
|
|
1683
|
+
const solution = await strategy.generateSolution({ poolSecret, poolId: custodianManageConfig.id, challengeId });
|
|
1684
|
+
challenges[challengeId] = await strategy.generateChallenge({ solution });
|
|
1685
|
+
}
|
|
1686
|
+
const custodianPool = {
|
|
1687
|
+
id: POOL_ID_CUSTODIAN_MANAGE,
|
|
1688
|
+
config: custodianManageConfig,
|
|
1689
|
+
challenges,
|
|
1690
|
+
isForeign: true
|
|
1691
|
+
};
|
|
1692
|
+
const keystone = await service.genesis({
|
|
1693
|
+
masterSecret: userSecret,
|
|
1694
|
+
configs: [userManageConfig],
|
|
1695
|
+
metaspace: mockMetaspace,
|
|
1696
|
+
space: mockSpace,
|
|
1697
|
+
});
|
|
1698
|
+
const keystoneWithCustodian = await service.addPools({
|
|
1699
|
+
latestKeystone: keystone,
|
|
1700
|
+
masterSecret: userSecret,
|
|
1701
|
+
newPools: [custodianPool],
|
|
1702
|
+
metaspace: mockMetaspace,
|
|
1703
|
+
space: mockSpace,
|
|
1704
|
+
});
|
|
1705
|
+
// Create an invalid frame manually where a foreign pool is rotated during change-password
|
|
1706
|
+
const badRotatedPool = await rotatePoolChallenges({
|
|
1707
|
+
prevPool: custodianPool,
|
|
1708
|
+
newMasterSecret: "bad_secret"
|
|
1709
|
+
});
|
|
1710
|
+
const evolved = await service.changePassword({
|
|
1711
|
+
latestKeystone: keystoneWithCustodian,
|
|
1712
|
+
newMasterSecret: "new_pass",
|
|
1713
|
+
signingSecret: userSecret,
|
|
1714
|
+
signingPoolId: POOL_ID_MANAGE,
|
|
1715
|
+
metaspace: mockMetaspace,
|
|
1716
|
+
space: mockSpace,
|
|
1717
|
+
});
|
|
1718
|
+
// Inject the foreign pool rotation manually to simulate an attacker
|
|
1719
|
+
const attackerData = {
|
|
1720
|
+
...evolved.data,
|
|
1721
|
+
challengePools: evolved.data.challengePools.map(p => p.id === POOL_ID_CUSTODIAN_MANAGE ? badRotatedPool : p)
|
|
1722
|
+
};
|
|
1723
|
+
// Update details claim to say we rotated custodian-manage
|
|
1724
|
+
attackerData.proofs[0].claim.details = {
|
|
1725
|
+
type: KeystoneClaimType.change_password,
|
|
1726
|
+
info: { change: [POOL_ID_MANAGE, POOL_ID_CUSTODIAN_MANAGE] }
|
|
1727
|
+
};
|
|
1728
|
+
const attackerKeystone = {
|
|
1729
|
+
...evolved,
|
|
1730
|
+
data: attackerData
|
|
1731
|
+
};
|
|
1732
|
+
const errors = await service.validate({
|
|
1733
|
+
prevIbGib: keystoneWithCustodian,
|
|
1734
|
+
currentIbGib: attackerKeystone
|
|
1735
|
+
});
|
|
1736
|
+
iReckon(sir, errors.length > 0).isGonnaBeTrue();
|
|
1737
|
+
const hasForeignViolation = errors.some(e => e.includes("Security Violation") && e.includes("foreign pool"));
|
|
1738
|
+
iReckon(sir, hasForeignViolation).isGonnaBeTrue();
|
|
1739
|
+
});
|
|
1740
|
+
});
|
|
1314
1741
|
//# sourceMappingURL=keystone-service-v1.respec.mjs.map
|