@ibgib/core-gib 0.1.64 → 0.1.65
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-helpers.d.mts +8 -0
- package/dist/keystone/keystone-helpers.d.mts.map +1 -1
- package/dist/keystone/keystone-helpers.mjs +74 -4
- package/dist/keystone/keystone-helpers.mjs.map +1 -1
- package/dist/keystone/keystone-service-v1.d.mts +31 -0
- package/dist/keystone/keystone-service-v1.d.mts.map +1 -1
- package/dist/keystone/keystone-service-v1.mjs +122 -1
- package/dist/keystone/keystone-service-v1.mjs.map +1 -1
- package/dist/keystone/keystone-service-v1.respec.mjs +168 -1
- package/dist/keystone/keystone-service-v1.respec.mjs.map +1 -1
- package/dist/keystone/keystone-types.d.mts +6 -1
- package/dist/keystone/keystone-types.d.mts.map +1 -1
- package/dist/keystone/keystone-types.mjs +2 -0
- package/dist/keystone/keystone-types.mjs.map +1 -1
- package/dist/keystone/policy/profiles/profile-domain.json +2 -46
- package/dist/keystone/policy/profiles/profile-sync.json +2 -16
- package/package.json +1 -1
- package/src/keystone/keystone-helpers.mts +89 -4
- package/src/keystone/keystone-service-v1.mts +150 -2
- package/src/keystone/keystone-service-v1.respec.mts +182 -1
- package/src/keystone/keystone-types.mts +7 -0
- package/src/keystone/policy/profiles/profile-domain.json +2 -46
- package/src/keystone/policy/profiles/profile-sync.json +2 -16
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
DelegateKeystoneInfo,
|
|
21
21
|
ClaimDetails,
|
|
22
22
|
ClaimDetails_AddPool,
|
|
23
|
+
ClaimDetails_RemovePool,
|
|
23
24
|
ClaimDetails_ReplacePool,
|
|
24
25
|
ClaimDetails_ChangePassword
|
|
25
26
|
} from "./keystone-types.mjs";
|
|
@@ -468,6 +469,13 @@ export async function solveAndReplenish({
|
|
|
468
469
|
|
|
469
470
|
const strategy = KeystoneStrategyFactory.create({ config: pool.config });
|
|
470
471
|
const poolSecret = await strategy.derivePoolSecret({ masterSecret });
|
|
472
|
+
|
|
473
|
+
// Verify secret correctness before solving
|
|
474
|
+
const isSecretCorrect = await verifyPoolSecret({ pool, signingSecret: masterSecret });
|
|
475
|
+
if (!isSecretCorrect) {
|
|
476
|
+
throw new Error(`Crypto Violation: Invalid signing secret for pool: ${pool.id} (E: 8a4c2b1d3e5f6a7b8c9d0e1f2a3b4c5f)`);
|
|
477
|
+
}
|
|
478
|
+
|
|
471
479
|
const solutions: KeystoneSolution[] = [];
|
|
472
480
|
|
|
473
481
|
// 1. Solve
|
|
@@ -877,6 +885,8 @@ export async function validateKeystoneTransition({
|
|
|
877
885
|
}
|
|
878
886
|
|
|
879
887
|
// Claim details verification (transition validation)
|
|
888
|
+
const signingPoolIds = new Set(currData.proofs.map(p => p.solutions?.[0]?.poolId).filter(Boolean));
|
|
889
|
+
|
|
880
890
|
for (const proof of currData.proofs) {
|
|
881
891
|
const claim = proof.claim;
|
|
882
892
|
if (claim?.details) {
|
|
@@ -918,11 +928,12 @@ export async function validateKeystoneTransition({
|
|
|
918
928
|
errors.push(`Existing pool ${prevPool.id} was removed during add-pool transition. (E: 5fa2a8cb28c9b60e68994511a5825)`);
|
|
919
929
|
continue;
|
|
920
930
|
}
|
|
921
|
-
// Verify configuration
|
|
931
|
+
// Verify configuration did not change
|
|
922
932
|
if (JSON.stringify(prevPool.config) !== JSON.stringify(currPool.config)) {
|
|
923
933
|
errors.push(`Existing pool config ${prevPool.id} was mutated during add-pool transition. (E: 6fa2a8cb28c9b60e68994511a5825)`);
|
|
924
934
|
}
|
|
925
|
-
|
|
935
|
+
// Verify challenges did not change, unless this was the signing pool used to authorize the transition
|
|
936
|
+
if (!signingPoolIds.has(prevPool.id) && JSON.stringify(prevPool.challenges) !== JSON.stringify(currPool.challenges)) {
|
|
926
937
|
errors.push(`Existing pool challenges ${prevPool.id} were mutated during add-pool transition. (E: 7fa2a8cb28c9b60e68994511a5825)`);
|
|
927
938
|
}
|
|
928
939
|
}
|
|
@@ -959,10 +970,52 @@ export async function validateKeystoneTransition({
|
|
|
959
970
|
if (JSON.stringify(prevPool.config) !== JSON.stringify(currPool.config)) {
|
|
960
971
|
errors.push(`Unrelated pool config ${prevPool.id} was mutated during replace-pool. (E: bfa2a8cb28c9b60e68994511a5825)`);
|
|
961
972
|
}
|
|
962
|
-
if (JSON.stringify(prevPool.challenges) !== JSON.stringify(currPool.challenges)) {
|
|
973
|
+
if (!signingPoolIds.has(prevPool.id) && JSON.stringify(prevPool.challenges) !== JSON.stringify(currPool.challenges)) {
|
|
963
974
|
errors.push(`Unrelated pool challenges ${prevPool.id} were mutated during replace-pool. (E: cfa2a8cb28c9b60e68994511a5825)`);
|
|
964
975
|
}
|
|
965
976
|
}
|
|
977
|
+
} else if (type === 'remove-pool') {
|
|
978
|
+
const removeInfo = info as ClaimDetails_RemovePool;
|
|
979
|
+
if (!removeInfo || !Array.isArray(removeInfo.remove) || removeInfo.remove.length === 0) {
|
|
980
|
+
errors.push(`details type is '${type}' but payload is invalid. (E: b7a2a8cb28c9b60e68994511a5825)`);
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const removeSet = new Set(removeInfo.remove);
|
|
985
|
+
|
|
986
|
+
// Verify that the set of pool IDs in current equals previous minus the removed pools
|
|
987
|
+
const prevIds = new Set(prevData.challengePools.map(p => p.id));
|
|
988
|
+
const currIds = new Set(currData.challengePools.map(p => p.id));
|
|
989
|
+
|
|
990
|
+
for (const id of removeInfo.remove) {
|
|
991
|
+
if (!prevIds.has(id)) {
|
|
992
|
+
errors.push(`ClaimDetails_RemovePool lists pool ${id} but it is missing in the previous keystone. (E: c7a2a8cb28c9b60e68994511a5825)`);
|
|
993
|
+
}
|
|
994
|
+
if (currIds.has(id)) {
|
|
995
|
+
errors.push(`ClaimDetails_RemovePool lists pool ${id} but it is still present in the evolved keystone. (E: d7a2a8cb28c9b60e68994511a5825)`);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Verify size
|
|
1000
|
+
if (currData.challengePools.length !== prevData.challengePools.length - removeInfo.remove.length) {
|
|
1001
|
+
errors.push(`Structural mismatch: expected ${prevData.challengePools.length - removeInfo.remove.length} pools, found ${currData.challengePools.length}. (E: e7a2a8cb28c9b60e68994511a5825)`);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Verify all remaining pools are unmodified
|
|
1005
|
+
for (const prevPool of prevData.challengePools) {
|
|
1006
|
+
if (removeSet.has(prevPool.id)) continue;
|
|
1007
|
+
const currPool = currData.challengePools.find(p => p.id === prevPool.id);
|
|
1008
|
+
if (!currPool) {
|
|
1009
|
+
errors.push(`Pool ${prevPool.id} was unexpectedly removed. (E: f7a2a8cb28c9b60e68994511a5825)`);
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
if (JSON.stringify(prevPool.config) !== JSON.stringify(currPool.config)) {
|
|
1013
|
+
errors.push(`Remaining pool config ${prevPool.id} was mutated during remove-pool. (E: 07a2a8cb28c9b60e68994511a5825)`);
|
|
1014
|
+
}
|
|
1015
|
+
if (!signingPoolIds.has(prevPool.id) && JSON.stringify(prevPool.challenges) !== JSON.stringify(currPool.challenges)) {
|
|
1016
|
+
errors.push(`Remaining pool challenges ${prevPool.id} were mutated during remove-pool. (E: 17a2a8cb28c9b60e68994511a5825)`);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
966
1019
|
|
|
967
1020
|
} else if (type === 'change-password') {
|
|
968
1021
|
const changeInfo = info as ClaimDetails_ChangePassword;
|
|
@@ -1004,7 +1057,7 @@ export async function validateKeystoneTransition({
|
|
|
1004
1057
|
if (JSON.stringify(prevPool.config) !== JSON.stringify(currPool.config)) {
|
|
1005
1058
|
errors.push(`Unrelated pool config ${prevPool.id} was mutated during change-password. (E: 02b2a8cb28c9b60e68994511a5825)`);
|
|
1006
1059
|
}
|
|
1007
|
-
if (JSON.stringify(prevPool.challenges) !== JSON.stringify(currPool.challenges)) {
|
|
1060
|
+
if (!signingPoolIds.has(prevPool.id) && JSON.stringify(prevPool.challenges) !== JSON.stringify(currPool.challenges)) {
|
|
1008
1061
|
errors.push(`Unrelated pool challenges ${prevPool.id} were mutated during change-password. (E: 03b2a8cb28c9b60e68994511a5825)`);
|
|
1009
1062
|
}
|
|
1010
1063
|
}
|
|
@@ -1488,4 +1541,36 @@ export async function rotatePoolChallenges({
|
|
|
1488
1541
|
...(prevPool.isForeign !== undefined ? { isForeign: prevPool.isForeign } : {}),
|
|
1489
1542
|
...(prevPool.metadata ? { metadata: prevPool.metadata } : {})
|
|
1490
1543
|
};
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
/**
|
|
1547
|
+
* Pure helper to verify if a candidate signing secret (passphrase) is correct for a specific challenge pool.
|
|
1548
|
+
* Does not mutate any state.
|
|
1549
|
+
*/
|
|
1550
|
+
export async function verifyPoolSecret({
|
|
1551
|
+
pool,
|
|
1552
|
+
signingSecret
|
|
1553
|
+
}: {
|
|
1554
|
+
pool: KeystoneChallengePool;
|
|
1555
|
+
signingSecret: string;
|
|
1556
|
+
}): Promise<boolean> {
|
|
1557
|
+
const challengeIds = Object.keys(pool.challenges);
|
|
1558
|
+
if (challengeIds.length === 0) return false;
|
|
1559
|
+
|
|
1560
|
+
try {
|
|
1561
|
+
const firstId = challengeIds[0];
|
|
1562
|
+
const challenge = pool.challenges[firstId];
|
|
1563
|
+
|
|
1564
|
+
const strategy = KeystoneStrategyFactory.create({ config: pool.config });
|
|
1565
|
+
const poolSecret = await strategy.derivePoolSecret({ masterSecret: signingSecret });
|
|
1566
|
+
const solution = await strategy.generateSolution({
|
|
1567
|
+
poolSecret,
|
|
1568
|
+
poolId: pool.id,
|
|
1569
|
+
challengeId: firstId
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
return await strategy.validateSolution({ solution, challenge });
|
|
1573
|
+
} catch {
|
|
1574
|
+
return false;
|
|
1575
|
+
}
|
|
1491
1576
|
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
KeystoneData_V1, KeystoneIbGib_V1, KeystonePoolConfig, KeystoneClaim,
|
|
9
9
|
KeystoneChallengePool, KeystoneReplenishStrategy, KeystoneRevocationInfo,
|
|
10
10
|
DelegateKeystoneStatus, KeystoneClaimType,
|
|
11
|
-
ClaimDetails_AddPool, ClaimDetails_ReplacePool, ClaimDetails_ChangePassword
|
|
11
|
+
ClaimDetails_AddPool, ClaimDetails_RemovePool, ClaimDetails_ReplacePool, ClaimDetails_ChangePassword
|
|
12
12
|
} from './keystone-types.mjs';
|
|
13
13
|
import { KeystoneStrategyFactory } from './strategy/keystone-strategy-factory.mjs';
|
|
14
14
|
import { createStandardPoolConfig } from './keystone-config-builder.mjs';
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
generateOpaqueChallengeId, resolveTargetPool, selectChallengeIds,
|
|
25
25
|
solveAndReplenish, validateKeystoneTransition,
|
|
26
26
|
validateGenesisKeystone, validateKeystoneGraph,
|
|
27
|
-
generatePoolChallenges, rotatePoolChallenges
|
|
27
|
+
generatePoolChallenges, rotatePoolChallenges, verifyPoolSecret
|
|
28
28
|
} from './keystone-helpers.mjs';
|
|
29
29
|
import { getLatestAddrs } from '../witness/space/space-helper.mjs';
|
|
30
30
|
import { IbGibSpaceAny } from '../witness/space/space-base-v1.mjs';
|
|
@@ -796,6 +796,128 @@ export class KeystoneService_V1 {
|
|
|
796
796
|
}
|
|
797
797
|
}
|
|
798
798
|
|
|
799
|
+
/**
|
|
800
|
+
* Structural evolution: Removes challenge pools from the keystone.
|
|
801
|
+
*
|
|
802
|
+
* Use Case: Unlinking a custodian or delegate SSO pool.
|
|
803
|
+
*
|
|
804
|
+
* Requires the Master Secret to authorize the change via a pool containing
|
|
805
|
+
* the 'manage' verb (the administrative manage pool).
|
|
806
|
+
*/
|
|
807
|
+
async removePools({
|
|
808
|
+
latestKeystone,
|
|
809
|
+
masterSecret,
|
|
810
|
+
poolIds,
|
|
811
|
+
metaspace,
|
|
812
|
+
space,
|
|
813
|
+
}: {
|
|
814
|
+
latestKeystone: KeystoneIbGib_V1;
|
|
815
|
+
/**
|
|
816
|
+
* Alice's Master Secret.
|
|
817
|
+
* Required to solve challenges from the Admin/Manage pool to authorize this change.
|
|
818
|
+
*/
|
|
819
|
+
masterSecret: string;
|
|
820
|
+
/**
|
|
821
|
+
* The IDs of the pools to remove.
|
|
822
|
+
*/
|
|
823
|
+
poolIds: string[];
|
|
824
|
+
metaspace: MetaspaceService;
|
|
825
|
+
space: IbGibSpaceAny;
|
|
826
|
+
}): Promise<KeystoneIbGib_V1> {
|
|
827
|
+
const lc = `${this.lc}[${this.removePools.name}]`;
|
|
828
|
+
try {
|
|
829
|
+
if (logalot) { console.log(`${lc} starting...`); }
|
|
830
|
+
|
|
831
|
+
if (!latestKeystone.data) { throw new Error(`(UNEXPECTED) latestKeystone.data falsy? (E: 8334c8faed128166a999d428c7805b25)`); }
|
|
832
|
+
const prevData = latestKeystone.data;
|
|
833
|
+
|
|
834
|
+
if (prevData.revocationInfo) { throw new Error(`Keystone has been revoked. Cannot remove pools. (E: 9599f8f51c78d722252ddb2894fdbe25)`); }
|
|
835
|
+
|
|
836
|
+
if (poolIds.length === 0) { throw new Error(`No pool IDs provided to remove. (E: 7599f8f51c78d722252ddb2894fdbe25)`); }
|
|
837
|
+
|
|
838
|
+
// 1. Identify Authority (Resolve Admin Pool)
|
|
839
|
+
const adminPool = resolveTargetPool({
|
|
840
|
+
pools: prevData.challengePools,
|
|
841
|
+
verb: KEYSTONE_VERB_MANAGE,
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
if (logalot) { console.log(`${lc} Authorized via pool: ${adminPool.id}`); }
|
|
845
|
+
|
|
846
|
+
// 2. Construct the Management Claim with typed details
|
|
847
|
+
const target = getIbGibAddr({ ibGib: latestKeystone });
|
|
848
|
+
const claim: Partial<KeystoneClaim> = {
|
|
849
|
+
verb: KEYSTONE_VERB_MANAGE,
|
|
850
|
+
target,
|
|
851
|
+
details: {
|
|
852
|
+
type: KeystoneClaimType.remove_pool,
|
|
853
|
+
info: { remove: poolIds } as ClaimDetails_RemovePool
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
// 3. Calculate Costs
|
|
858
|
+
const idsToSolve = await selectChallengeIds({
|
|
859
|
+
pool: adminPool,
|
|
860
|
+
targetAddr: target
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
// 4. Pay the Cost (Solve & Replenish)
|
|
864
|
+
const { proof, nextPools: replenishedExistingPools } = await solveAndReplenish({
|
|
865
|
+
targetPoolId: adminPool.id,
|
|
866
|
+
prevPools: prevData.challengePools,
|
|
867
|
+
masterSecret,
|
|
868
|
+
challengeIds: idsToSolve,
|
|
869
|
+
claim,
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
// 5. Mutate Structure (Remove the pools)
|
|
873
|
+
const removeSet = new Set(poolIds);
|
|
874
|
+
const finalPools = replenishedExistingPools.filter(p => !removeSet.has(p.id));
|
|
875
|
+
|
|
876
|
+
// Verify that we actually removed the expected count of pools
|
|
877
|
+
if (finalPools.length !== replenishedExistingPools.length - poolIds.length) {
|
|
878
|
+
const missingIds = poolIds.filter(id => !replenishedExistingPools.some(p => p.id === id));
|
|
879
|
+
throw new Error(`Cannot remove pool. Pool IDs not found in keystone: ${missingIds.join(', ')} (E: 8a4c2b1d3e5f6a7b8c9d0e1f2a3b4c5e)`);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// 6. Construct New Data
|
|
883
|
+
const n = (prevData.n ?? 0) + 1;
|
|
884
|
+
let checkpointDetails: any | undefined;
|
|
885
|
+
if (n % KEYSTONE_CHECKPOINT_FREQUENCY === 0) {
|
|
886
|
+
const currentFrameView: KeystoneIbGib_V1 = {
|
|
887
|
+
...latestKeystone,
|
|
888
|
+
data: { ...prevData, challengePools: finalPools }
|
|
889
|
+
};
|
|
890
|
+
checkpointDetails = await this.getAggregateDetails({
|
|
891
|
+
latestKeystone: currentFrameView,
|
|
892
|
+
metaspace,
|
|
893
|
+
space
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const newData: KeystoneData_V1 = {
|
|
898
|
+
challengePools: finalPools,
|
|
899
|
+
proofs: [proof],
|
|
900
|
+
checkpointDetails,
|
|
901
|
+
...(prevData.delegates ? { delegates: prevData.delegates } : {}),
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
// 7. Commit
|
|
905
|
+
const newKeystone = await evolvePersistAndRegisterKeystone({
|
|
906
|
+
prevIbGib: latestKeystone,
|
|
907
|
+
newData,
|
|
908
|
+
metaspace,
|
|
909
|
+
space
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
return newKeystone;
|
|
913
|
+
} catch (error) {
|
|
914
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
915
|
+
throw error;
|
|
916
|
+
} finally {
|
|
917
|
+
if (logalot) { console.log(`${lc} complete.`); }
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
799
921
|
/**
|
|
800
922
|
* Consolidates the state of a Keystone identity across its history.
|
|
801
923
|
* Walks back until it hits a checkpoint or genesis.
|
|
@@ -1213,4 +1335,30 @@ export class KeystoneService_V1 {
|
|
|
1213
1335
|
throw error;
|
|
1214
1336
|
}
|
|
1215
1337
|
}
|
|
1338
|
+
|
|
1339
|
+
/**
|
|
1340
|
+
* Verifies if a candidate signing secret (passphrase) is correct for a specific challenge pool.
|
|
1341
|
+
* Does not mutate any state.
|
|
1342
|
+
*/
|
|
1343
|
+
async verifySigningSecret({
|
|
1344
|
+
keystoneIbGib,
|
|
1345
|
+
signingSecret,
|
|
1346
|
+
poolId
|
|
1347
|
+
}: {
|
|
1348
|
+
keystoneIbGib: KeystoneIbGib_V1;
|
|
1349
|
+
signingSecret: string;
|
|
1350
|
+
poolId: string;
|
|
1351
|
+
}): Promise<boolean> {
|
|
1352
|
+
const lc = `${this.lc}[${this.verifySigningSecret.name}]`;
|
|
1353
|
+
try {
|
|
1354
|
+
if (!keystoneIbGib.data) return false;
|
|
1355
|
+
const pool = keystoneIbGib.data.challengePools.find(p => p.id === poolId);
|
|
1356
|
+
if (!pool) return false;
|
|
1357
|
+
|
|
1358
|
+
return await verifyPoolSecret({ pool, signingSecret });
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
console.warn(`${lc} Exception during verification: ${extractErrorMsg(error)}`);
|
|
1361
|
+
return false;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1216
1364
|
}
|
|
@@ -1167,7 +1167,7 @@ await respecfully(sir, 'Suite F: Deep Inspection', async () => {
|
|
|
1167
1167
|
.withDescription('Alice domain keystone');
|
|
1168
1168
|
|
|
1169
1169
|
const configs = await builder.compileConfigs();
|
|
1170
|
-
iReckon(sir, configs.length).asTo('number of pools').willEqual(
|
|
1170
|
+
iReckon(sir, configs.length).asTo('number of pools').willEqual(1);
|
|
1171
1171
|
|
|
1172
1172
|
const connectPool = configs.find(c => c.id === 'connect');
|
|
1173
1173
|
iReckon(sir, connectPool).asTo('connect pool omitted').isGonnaBeUndefined();
|
|
@@ -1964,6 +1964,61 @@ await respecfully(sir, 'Suite G: Combined Pools & Password Rotation', async () =
|
|
|
1964
1964
|
currentIbGib: rotated
|
|
1965
1965
|
});
|
|
1966
1966
|
iReckon(sir, errors.length).willEqual(0);
|
|
1967
|
+
|
|
1968
|
+
// Verify we can sign using the rotated 'manage' pool with the new passphrase
|
|
1969
|
+
const manageClaim = { verb: KEYSTONE_VERB_MANAGE, target: "test_target_manage" };
|
|
1970
|
+
const signedWithNewManage = await service.sign({
|
|
1971
|
+
latestKeystone: rotated,
|
|
1972
|
+
masterSecret: newPassphrase,
|
|
1973
|
+
claim: manageClaim,
|
|
1974
|
+
poolId: POOL_ID_MANAGE,
|
|
1975
|
+
metaspace: mockMetaspace,
|
|
1976
|
+
space: mockSpace as any,
|
|
1977
|
+
});
|
|
1978
|
+
iReckon(sir, signedWithNewManage).isGonnaBeTruthy();
|
|
1979
|
+
|
|
1980
|
+
// Verify we can sign using the rotated 'secondary-native' pool with the new passphrase
|
|
1981
|
+
const syncClaim = { verb: KEYSTONE_VERB_SYNC, target: "test_target_sync" };
|
|
1982
|
+
const signedWithNewSync = await service.sign({
|
|
1983
|
+
latestKeystone: rotated,
|
|
1984
|
+
masterSecret: newPassphrase,
|
|
1985
|
+
claim: syncClaim,
|
|
1986
|
+
poolId: "secondary-native",
|
|
1987
|
+
metaspace: mockMetaspace,
|
|
1988
|
+
space: mockSpace as any,
|
|
1989
|
+
});
|
|
1990
|
+
iReckon(sir, signedWithNewSync).isGonnaBeTruthy();
|
|
1991
|
+
|
|
1992
|
+
// Verify signing with the OLD master secret fails on both pools
|
|
1993
|
+
let oldManageFailed = false;
|
|
1994
|
+
try {
|
|
1995
|
+
await service.sign({
|
|
1996
|
+
latestKeystone: rotated,
|
|
1997
|
+
masterSecret: userSecret,
|
|
1998
|
+
claim: manageClaim,
|
|
1999
|
+
poolId: POOL_ID_MANAGE,
|
|
2000
|
+
metaspace: mockMetaspace,
|
|
2001
|
+
space: mockSpace as any,
|
|
2002
|
+
});
|
|
2003
|
+
} catch {
|
|
2004
|
+
oldManageFailed = true;
|
|
2005
|
+
}
|
|
2006
|
+
iReckon(sir, oldManageFailed).isGonnaBeTrue();
|
|
2007
|
+
|
|
2008
|
+
let oldSyncFailed = false;
|
|
2009
|
+
try {
|
|
2010
|
+
await service.sign({
|
|
2011
|
+
latestKeystone: rotated,
|
|
2012
|
+
masterSecret: userSecret,
|
|
2013
|
+
claim: syncClaim,
|
|
2014
|
+
poolId: "secondary-native",
|
|
2015
|
+
metaspace: mockMetaspace,
|
|
2016
|
+
space: mockSpace as any,
|
|
2017
|
+
});
|
|
2018
|
+
} catch {
|
|
2019
|
+
oldSyncFailed = true;
|
|
2020
|
+
}
|
|
2021
|
+
iReckon(sir, oldSyncFailed).isGonnaBeTrue();
|
|
1967
2022
|
});
|
|
1968
2023
|
|
|
1969
2024
|
await ifWeMight(sir, 'fails validation if password rotation details or constraints are violated', async () => {
|
|
@@ -2046,4 +2101,130 @@ await respecfully(sir, 'Suite G: Combined Pools & Password Rotation', async () =
|
|
|
2046
2101
|
const hasForeignViolation = errors.some(e => e.includes("Security Violation") && e.includes("foreign pool"));
|
|
2047
2102
|
iReckon(sir, hasForeignViolation).isGonnaBeTrue();
|
|
2048
2103
|
});
|
|
2104
|
+
|
|
2105
|
+
await ifWeMight(sir, 'verifies signing secrets correctly for a pool', async () => {
|
|
2106
|
+
const userSecret = "alice_secret_123";
|
|
2107
|
+
const builder = KeystoneProfileBuilder.buildKeystone('low')
|
|
2108
|
+
.withUsername('alice')
|
|
2109
|
+
.withEmail('alice@example.com')
|
|
2110
|
+
.withDescription('Alice test keystone');
|
|
2111
|
+
|
|
2112
|
+
const configs = await builder.compileConfigs();
|
|
2113
|
+
const keystone = await service.genesis({
|
|
2114
|
+
masterSecret: userSecret,
|
|
2115
|
+
configs,
|
|
2116
|
+
frameDetails: builder.getFrameDetails(),
|
|
2117
|
+
metaspace: mockMetaspace,
|
|
2118
|
+
space: mockSpace as any,
|
|
2119
|
+
});
|
|
2120
|
+
|
|
2121
|
+
// 1. Correct secret on manage pool
|
|
2122
|
+
const isCorrectManage = await service.verifySigningSecret({
|
|
2123
|
+
keystoneIbGib: keystone,
|
|
2124
|
+
signingSecret: userSecret,
|
|
2125
|
+
poolId: POOL_ID_MANAGE
|
|
2126
|
+
});
|
|
2127
|
+
iReckon(sir, isCorrectManage).asTo('correct manage secret').isGonnaBeTrue();
|
|
2128
|
+
|
|
2129
|
+
// 2. Incorrect secret on manage pool
|
|
2130
|
+
const isWrongManage = await service.verifySigningSecret({
|
|
2131
|
+
keystoneIbGib: keystone,
|
|
2132
|
+
signingSecret: "wrong_secret",
|
|
2133
|
+
poolId: POOL_ID_MANAGE
|
|
2134
|
+
});
|
|
2135
|
+
iReckon(sir, isWrongManage).asTo('incorrect manage secret').isGonnaBeFalse();
|
|
2136
|
+
|
|
2137
|
+
// 3. Correct secret on default/sign pool
|
|
2138
|
+
const isCorrectDefault = await service.verifySigningSecret({
|
|
2139
|
+
keystoneIbGib: keystone,
|
|
2140
|
+
signingSecret: userSecret,
|
|
2141
|
+
poolId: 'default'
|
|
2142
|
+
});
|
|
2143
|
+
iReckon(sir, isCorrectDefault).asTo('correct default secret').isGonnaBeTrue();
|
|
2144
|
+
|
|
2145
|
+
// 4. Invalid/non-existent pool ID
|
|
2146
|
+
const isInvalidPool = await service.verifySigningSecret({
|
|
2147
|
+
keystoneIbGib: keystone,
|
|
2148
|
+
signingSecret: userSecret,
|
|
2149
|
+
poolId: 'invalid_pool_id_123'
|
|
2150
|
+
});
|
|
2151
|
+
iReckon(sir, isInvalidPool).asTo('invalid pool id').isGonnaBeFalse();
|
|
2152
|
+
});
|
|
2153
|
+
|
|
2154
|
+
await ifWeMight(sir, 'removes pools successfully, validating transitions and auth', async () => {
|
|
2155
|
+
const userSecret = "alice_secret_123";
|
|
2156
|
+
const customPoolId = "custodian-manage-google";
|
|
2157
|
+
const configs = [
|
|
2158
|
+
createStandardPoolConfig({
|
|
2159
|
+
id: POOL_ID_MANAGE,
|
|
2160
|
+
salt: "manage_salt",
|
|
2161
|
+
}),
|
|
2162
|
+
createStandardPoolConfig({
|
|
2163
|
+
id: customPoolId,
|
|
2164
|
+
salt: "custodian_salt",
|
|
2165
|
+
})
|
|
2166
|
+
];
|
|
2167
|
+
|
|
2168
|
+
const keystone = await service.genesis({
|
|
2169
|
+
masterSecret: userSecret,
|
|
2170
|
+
configs,
|
|
2171
|
+
metaspace: mockMetaspace,
|
|
2172
|
+
space: mockSpace as any,
|
|
2173
|
+
});
|
|
2174
|
+
|
|
2175
|
+
// 1. Success path: remove customPoolId using correct secret
|
|
2176
|
+
const evolvedKeystone = await service.removePools({
|
|
2177
|
+
latestKeystone: keystone,
|
|
2178
|
+
masterSecret: userSecret,
|
|
2179
|
+
poolIds: [customPoolId],
|
|
2180
|
+
metaspace: mockMetaspace,
|
|
2181
|
+
space: mockSpace as any,
|
|
2182
|
+
});
|
|
2183
|
+
|
|
2184
|
+
iReckon(sir, evolvedKeystone).asTo('evolved keystone exists').isGonnaBeTruthy();
|
|
2185
|
+
iReckon(sir, evolvedKeystone.data?.challengePools.length).asTo('pool count reduced').willEqual(1);
|
|
2186
|
+
iReckon(sir, evolvedKeystone.data?.challengePools[0].id).asTo('manage pool remains').willEqual(POOL_ID_MANAGE);
|
|
2187
|
+
|
|
2188
|
+
// Verify transition is fully valid
|
|
2189
|
+
const errors = await service.validate({
|
|
2190
|
+
prevIbGib: keystone,
|
|
2191
|
+
currentIbGib: evolvedKeystone
|
|
2192
|
+
});
|
|
2193
|
+
iReckon(sir, errors.length).asTo('transition validation succeeds').willEqual(0);
|
|
2194
|
+
|
|
2195
|
+
// 2. Sad path: invalid master secret
|
|
2196
|
+
let cryptoErrorCaught = false;
|
|
2197
|
+
try {
|
|
2198
|
+
await service.removePools({
|
|
2199
|
+
latestKeystone: keystone,
|
|
2200
|
+
masterSecret: "wrong_secret",
|
|
2201
|
+
poolIds: [customPoolId],
|
|
2202
|
+
metaspace: mockMetaspace,
|
|
2203
|
+
space: mockSpace as any,
|
|
2204
|
+
});
|
|
2205
|
+
} catch (e: any) {
|
|
2206
|
+
if (e.message.includes('Crypto Violation')) {
|
|
2207
|
+
cryptoErrorCaught = true;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
iReckon(sir, cryptoErrorCaught).asTo('forgery check prevents removal').isGonnaBeTrue();
|
|
2211
|
+
|
|
2212
|
+
// 3. Sad path: non-existent pool ID
|
|
2213
|
+
let notFoundErrorCaught = false;
|
|
2214
|
+
try {
|
|
2215
|
+
await service.removePools({
|
|
2216
|
+
latestKeystone: keystone,
|
|
2217
|
+
masterSecret: userSecret,
|
|
2218
|
+
poolIds: ["non-existent-pool"],
|
|
2219
|
+
metaspace: mockMetaspace,
|
|
2220
|
+
space: mockSpace as any,
|
|
2221
|
+
});
|
|
2222
|
+
} catch (e: any) {
|
|
2223
|
+
if (e.message.includes('not found in keystone')) {
|
|
2224
|
+
notFoundErrorCaught = true;
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
iReckon(sir, notFoundErrorCaught).asTo('non-existent pool ID rejected').isGonnaBeTrue();
|
|
2228
|
+
});
|
|
2049
2229
|
});
|
|
2230
|
+
|
|
@@ -258,17 +258,20 @@ export interface KeystoneChallengePool {
|
|
|
258
258
|
|
|
259
259
|
// #region KeystoneClaimType enum
|
|
260
260
|
export const KEYSTONE_CLAIM_TYPE_ADD_POOL = 'add-pool';
|
|
261
|
+
export const KEYSTONE_CLAIM_TYPE_REMOVE_POOL = 'remove-pool';
|
|
261
262
|
export const KEYSTONE_CLAIM_TYPE_REPLACE_POOL = 'replace-pool';
|
|
262
263
|
export const KEYSTONE_CLAIM_TYPE_CHANGE_PASSWORD = 'change-password';
|
|
263
264
|
|
|
264
265
|
export type KeystoneClaimType =
|
|
265
266
|
| typeof KEYSTONE_CLAIM_TYPE_ADD_POOL
|
|
267
|
+
| typeof KEYSTONE_CLAIM_TYPE_REMOVE_POOL
|
|
266
268
|
| typeof KEYSTONE_CLAIM_TYPE_REPLACE_POOL
|
|
267
269
|
| typeof KEYSTONE_CLAIM_TYPE_CHANGE_PASSWORD
|
|
268
270
|
;
|
|
269
271
|
|
|
270
272
|
export const KeystoneClaimType = {
|
|
271
273
|
add_pool: KEYSTONE_CLAIM_TYPE_ADD_POOL,
|
|
274
|
+
remove_pool: KEYSTONE_CLAIM_TYPE_REMOVE_POOL,
|
|
272
275
|
replace_pool: KEYSTONE_CLAIM_TYPE_REPLACE_POOL,
|
|
273
276
|
change_password: KEYSTONE_CLAIM_TYPE_CHANGE_PASSWORD,
|
|
274
277
|
} satisfies { readonly [key: string]: KeystoneClaimType };
|
|
@@ -289,6 +292,10 @@ export interface ClaimDetails_AddPool {
|
|
|
289
292
|
add: string[];
|
|
290
293
|
}
|
|
291
294
|
|
|
295
|
+
export interface ClaimDetails_RemovePool {
|
|
296
|
+
remove: string[];
|
|
297
|
+
}
|
|
298
|
+
|
|
292
299
|
export interface ClaimDetails_ReplacePool {
|
|
293
300
|
replace: string;
|
|
294
301
|
}
|
|
@@ -5,25 +5,11 @@
|
|
|
5
5
|
"securityLevel": "high"
|
|
6
6
|
},
|
|
7
7
|
"pools": {
|
|
8
|
-
"sync": {
|
|
9
|
-
"id": "sync",
|
|
10
|
-
"allowedVerbs": [
|
|
11
|
-
"sync"
|
|
12
|
-
],
|
|
13
|
-
"algo": "SHA-512",
|
|
14
|
-
"rounds": 4,
|
|
15
|
-
"behavior": {
|
|
16
|
-
"size": 2000,
|
|
17
|
-
"replenish": "top-up",
|
|
18
|
-
"selectSequentially": 10,
|
|
19
|
-
"selectRandomly": 10,
|
|
20
|
-
"targetBindingCount": 16
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
8
|
"manage": {
|
|
24
9
|
"id": "manage",
|
|
25
10
|
"allowedVerbs": [
|
|
26
|
-
"manage"
|
|
11
|
+
"manage",
|
|
12
|
+
"revoke"
|
|
27
13
|
],
|
|
28
14
|
"algo": "SHA-512",
|
|
29
15
|
"rounds": 6,
|
|
@@ -34,36 +20,6 @@
|
|
|
34
20
|
"selectRandomly": 12,
|
|
35
21
|
"targetBindingCount": 16
|
|
36
22
|
}
|
|
37
|
-
},
|
|
38
|
-
"revoke": {
|
|
39
|
-
"id": "revoke",
|
|
40
|
-
"allowedVerbs": [
|
|
41
|
-
"revoke"
|
|
42
|
-
],
|
|
43
|
-
"algo": "SHA-512",
|
|
44
|
-
"rounds": 20,
|
|
45
|
-
"behavior": {
|
|
46
|
-
"size": 2000,
|
|
47
|
-
"replenish": "delete-all",
|
|
48
|
-
"selectSequentially": 50,
|
|
49
|
-
"selectRandomly": 50,
|
|
50
|
-
"targetBindingCount": 0
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
"sign": {
|
|
54
|
-
"id": "default",
|
|
55
|
-
"allowedVerbs": [
|
|
56
|
-
"sign"
|
|
57
|
-
],
|
|
58
|
-
"algo": "SHA-512",
|
|
59
|
-
"rounds": 4,
|
|
60
|
-
"behavior": {
|
|
61
|
-
"size": 2000,
|
|
62
|
-
"replenish": "top-up",
|
|
63
|
-
"selectSequentially": 10,
|
|
64
|
-
"selectRandomly": 10,
|
|
65
|
-
"targetBindingCount": 16
|
|
66
|
-
}
|
|
67
23
|
}
|
|
68
24
|
}
|
|
69
25
|
}
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"manage": {
|
|
24
24
|
"id": "manage",
|
|
25
25
|
"allowedVerbs": [
|
|
26
|
-
"manage"
|
|
26
|
+
"manage",
|
|
27
|
+
"revoke"
|
|
27
28
|
],
|
|
28
29
|
"algo": "SHA-256",
|
|
29
30
|
"rounds": 2,
|
|
@@ -35,21 +36,6 @@
|
|
|
35
36
|
"targetBindingCount": 8
|
|
36
37
|
}
|
|
37
38
|
},
|
|
38
|
-
"revoke": {
|
|
39
|
-
"id": "revoke",
|
|
40
|
-
"allowedVerbs": [
|
|
41
|
-
"revoke"
|
|
42
|
-
],
|
|
43
|
-
"algo": "SHA-256",
|
|
44
|
-
"rounds": 5,
|
|
45
|
-
"behavior": {
|
|
46
|
-
"size": 100,
|
|
47
|
-
"replenish": "delete-all",
|
|
48
|
-
"selectSequentially": 3,
|
|
49
|
-
"selectRandomly": 3,
|
|
50
|
-
"targetBindingCount": 0
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
39
|
"connect": {
|
|
54
40
|
"id": "connect",
|
|
55
41
|
"allowedVerbs": [
|