@learncard/sss-key-manager 0.2.1 → 0.2.2
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/escrow-attestation.d.ts +2 -9
- package/dist/escrow-attestation.d.ts.map +1 -1
- package/dist/escrow-authority.d.ts +112 -0
- package/dist/escrow-authority.d.ts.map +1 -0
- package/dist/escrow-authority.types.d.ts +51 -0
- package/dist/escrow-authority.types.d.ts.map +1 -0
- package/dist/escrow-crypto.d.ts +9 -1
- package/dist/escrow-crypto.d.ts.map +1 -1
- package/dist/escrow-enrollment-intent.d.ts +13 -0
- package/dist/escrow-enrollment-intent.d.ts.map +1 -0
- package/dist/escrow-enrollment-intent.types.d.ts +28 -0
- package/dist/escrow-enrollment-intent.types.d.ts.map +1 -0
- package/dist/escrow-nitro-attestation.d.ts +2 -0
- package/dist/escrow-nitro-attestation.d.ts.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/sss-key-manager.cjs.development.js +1592 -213
- package/dist/sss-key-manager.cjs.development.js.map +4 -4
- package/dist/sss-key-manager.cjs.production.min.js +28 -23
- package/dist/sss-key-manager.cjs.production.min.js.map +4 -4
- package/dist/sss-key-manager.esm.js +1107 -163
- package/dist/sss-key-manager.esm.js.map +4 -4
- package/dist/sss-strategy.d.ts.map +1 -1
- package/dist/types.d.ts +16 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__fixtures__/escrow-authority-v1.json +96 -0
- package/src/__fixtures__/escrow-carry-contact-contract-v1.json +33 -0
- package/src/__fixtures__/escrow-enrollment-intent-v1.json +26 -0
- package/src/__fixtures__/nitro-attestation-real/staging-nsm-document.json +16 -0
- package/src/escrow-attestation.ts +10 -9
- package/src/escrow-authority.test.ts +643 -0
- package/src/escrow-authority.ts +597 -0
- package/src/escrow-authority.types.ts +56 -0
- package/src/escrow-crypto.ts +39 -1
- package/src/escrow-enrollment-intent.test.ts +224 -0
- package/src/escrow-enrollment-intent.ts +236 -0
- package/src/escrow-enrollment-intent.types.ts +28 -0
- package/src/escrow-nitro-attestation.test.ts +72 -0
- package/src/escrow-nitro-attestation.ts +34 -3
- package/src/index.ts +27 -0
- package/src/sss-authority.test.ts +889 -0
- package/src/sss-strategy.ts +479 -38
- package/src/types.ts +22 -3
package/src/sss-strategy.ts
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
ESCROW_PIN_LOCKED_MESSAGE,
|
|
44
44
|
ESCROW_PIN_MISMATCH_PATTERN,
|
|
45
45
|
ESCROW_PIN_UNAVAILABLE_MESSAGE,
|
|
46
|
+
type EscrowRecoveryExpectations,
|
|
46
47
|
} from '@learncard/types';
|
|
47
48
|
import {
|
|
48
49
|
validatePin,
|
|
@@ -52,6 +53,19 @@ import {
|
|
|
52
53
|
} from './escrow-pin';
|
|
53
54
|
import { encryptEscrowBlob, generateEscrowKeyPair, openEscrowRelease } from './escrow-crypto';
|
|
54
55
|
import { verifyEnclaveAttestation } from './escrow-attestation';
|
|
56
|
+
import {
|
|
57
|
+
createEscrowEnrollmentIntent,
|
|
58
|
+
resolveEscrowEnrollmentAction,
|
|
59
|
+
verifyEscrowAuthorityState,
|
|
60
|
+
verifyEscrowEnrollmentCommit,
|
|
61
|
+
verifyEscrowHoldRecord,
|
|
62
|
+
verifyEscrowHoldConfirmation,
|
|
63
|
+
escrowAuthorityMeasurements,
|
|
64
|
+
type EscrowHoldRecordLike,
|
|
65
|
+
} from './escrow-authority';
|
|
66
|
+
import type { VerifiedEnclaveIdentity, EscrowAuthorityState } from './escrow-authority.types';
|
|
67
|
+
import { EscrowEvidenceError } from './escrow-authority.types';
|
|
68
|
+
import type { EscrowEnvelope } from './escrow-crypto';
|
|
55
69
|
import type { EmailRelayBranding } from './email-relay-crypto';
|
|
56
70
|
|
|
57
71
|
import {
|
|
@@ -354,6 +368,7 @@ interface IdentityRecoverySessionResponse {
|
|
|
354
368
|
credentialId?: string;
|
|
355
369
|
shareVersion?: number;
|
|
356
370
|
}>;
|
|
371
|
+
primaryDid: string | null;
|
|
357
372
|
}
|
|
358
373
|
|
|
359
374
|
interface IdentityRecoveryMaterialResponse {
|
|
@@ -973,8 +988,12 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
973
988
|
|
|
974
989
|
// Capture before any await (including the rotation lock). A forgotten device
|
|
975
990
|
// must not be restored by a late write or by atomic-update rollback.
|
|
976
|
-
const guardedStorage = (): SSSStorageFunctions => {
|
|
991
|
+
const guardedStorage = (): SSSStorageFunctions & { assertCurrent: () => void } => {
|
|
977
992
|
const generation = storageGeneration;
|
|
993
|
+
const assertCurrent = (): void => {
|
|
994
|
+
if (generation !== storageGeneration)
|
|
995
|
+
throw new Error('This recovery request was cancelled.');
|
|
996
|
+
};
|
|
978
997
|
const write = (operation: () => Promise<void>): Promise<void> => {
|
|
979
998
|
if (generation !== storageGeneration) {
|
|
980
999
|
return Promise.reject(new Error('This recovery request was cancelled.'));
|
|
@@ -983,8 +1002,9 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
983
1002
|
localWrites.add(pending);
|
|
984
1003
|
return pending.finally(() => localWrites.delete(pending));
|
|
985
1004
|
};
|
|
986
|
-
const guarded: SSSStorageFunctions = {
|
|
1005
|
+
const guarded: SSSStorageFunctions & { assertCurrent: () => void } = {
|
|
987
1006
|
...storage,
|
|
1007
|
+
assertCurrent,
|
|
988
1008
|
storeDeviceShare: (share, id) => write(() => storage.storeDeviceShare(share, id)),
|
|
989
1009
|
storeShareVersion: (version, id) => write(() => storage.storeShareVersion(version, id)),
|
|
990
1010
|
clearAllShares: id => write(() => storage.clearAllShares(id)),
|
|
@@ -1058,6 +1078,7 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1058
1078
|
// (i.e. once per app session); a failed attempt is logged and left for
|
|
1059
1079
|
// the next session rather than retried on every subsequent call.
|
|
1060
1080
|
let staleEscrowReenrollAttempted = false;
|
|
1081
|
+
let verifiedEscrowEnvelope: EscrowEnvelope | undefined;
|
|
1061
1082
|
|
|
1062
1083
|
// Material from a rotation whose escrow POST failed. Retained in memory so a
|
|
1063
1084
|
// retry within this session re-posts instead of burning another share version.
|
|
@@ -1145,7 +1166,7 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1145
1166
|
}
|
|
1146
1167
|
};
|
|
1147
1168
|
|
|
1148
|
-
const fetchVerifiedEnclaveKey = async (): Promise<
|
|
1169
|
+
const fetchVerifiedEnclaveKey = async (): Promise<VerifiedEnclaveIdentity> => {
|
|
1149
1170
|
if (!config.escrow?.enabled) throw new Error('Escrow enrollment is disabled');
|
|
1150
1171
|
const nonce = crypto.getRandomValues(new Uint8Array(32));
|
|
1151
1172
|
const nonceHex = Array.from(nonce, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
@@ -1159,6 +1180,113 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1159
1180
|
return verifyEnclaveAttestation(attestation, config.escrow.attestation, nonce);
|
|
1160
1181
|
};
|
|
1161
1182
|
|
|
1183
|
+
/** Shared by the public `getEscrowRecoveryStatus` and [D]'s internal
|
|
1184
|
+
* confirm-time verification, so both read the enclave-signed
|
|
1185
|
+
* `holdRecord` field via this package's own `EscrowHoldStatus` type
|
|
1186
|
+
* rather than the narrower cross-package `KeyDerivationStrategy`
|
|
1187
|
+
* interface (which never exposes it to generic callers). */
|
|
1188
|
+
const fetchEscrowHoldStatus = async (
|
|
1189
|
+
params:
|
|
1190
|
+
| { holdId: string; resumeToken: string }
|
|
1191
|
+
| { token: string; providerType: AuthProviderType }
|
|
1192
|
+
): Promise<EscrowHoldStatus | null> => {
|
|
1193
|
+
const query = new URLSearchParams(
|
|
1194
|
+
'holdId' in params ? { holdId: params.holdId } : { providerType: params.providerType }
|
|
1195
|
+
);
|
|
1196
|
+
const secret = 'holdId' in params ? params.resumeToken : params.token;
|
|
1197
|
+
const result = await escrowRequest<{ hold: EscrowHoldStatus | null }>(`/status?${query}`, {
|
|
1198
|
+
method: 'GET',
|
|
1199
|
+
headers: {
|
|
1200
|
+
...buildHeaders('', undefined, tenantId),
|
|
1201
|
+
'X-Auth-Token': secret,
|
|
1202
|
+
},
|
|
1203
|
+
});
|
|
1204
|
+
return result.hold;
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
const recoveryExpectations = (
|
|
1208
|
+
holdId: string,
|
|
1209
|
+
expected?: EscrowRecoveryExpectations
|
|
1210
|
+
): EscrowRecoveryExpectations => {
|
|
1211
|
+
if (
|
|
1212
|
+
!expected ||
|
|
1213
|
+
expected.holdId !== holdId ||
|
|
1214
|
+
expected.tenant !== (tenantId ?? 'learncard') ||
|
|
1215
|
+
!expected.did ||
|
|
1216
|
+
!expected.clientEphemeralPublicKey ||
|
|
1217
|
+
!expected.blobHash ||
|
|
1218
|
+
!expected.authorityGeneration ||
|
|
1219
|
+
!expected.holdDigest ||
|
|
1220
|
+
!expected.ledgerPublicKey ||
|
|
1221
|
+
!expected.keyId
|
|
1222
|
+
)
|
|
1223
|
+
throw new EscrowEvidenceError();
|
|
1224
|
+
// Do not let callers mutate the bindings while attestation/network IO is pending.
|
|
1225
|
+
return { ...expected };
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
const verifyPendingHold = async (
|
|
1229
|
+
status: EscrowHoldStatus,
|
|
1230
|
+
expected: EscrowRecoveryExpectations,
|
|
1231
|
+
identity: VerifiedEnclaveIdentity
|
|
1232
|
+
): Promise<EscrowHoldRecordLike> => {
|
|
1233
|
+
if (
|
|
1234
|
+
!config.escrow ||
|
|
1235
|
+
identity.mode !== 'nitro' ||
|
|
1236
|
+
!identity.authority ||
|
|
1237
|
+
identity.authority.ledgerPublicKey !== expected.ledgerPublicKey ||
|
|
1238
|
+
![identity.keyId, ...identity.previousKeyIds].includes(expected.keyId) ||
|
|
1239
|
+
status.holdId !== expected.holdId ||
|
|
1240
|
+
!status.holdRecord
|
|
1241
|
+
)
|
|
1242
|
+
throw new EscrowEvidenceError();
|
|
1243
|
+
const record = status.holdRecord;
|
|
1244
|
+
const verified = await verifyEscrowHoldRecord(record, expected, identity, {
|
|
1245
|
+
historical: true,
|
|
1246
|
+
allowedMeasurements: await escrowAuthorityMeasurements(config.escrow.attestation),
|
|
1247
|
+
authorityGeneration: expected.authorityGeneration,
|
|
1248
|
+
});
|
|
1249
|
+
if (
|
|
1250
|
+
verified.holdDigest !== expected.holdDigest ||
|
|
1251
|
+
verified.receipt.keyId !== expected.keyId
|
|
1252
|
+
)
|
|
1253
|
+
throw new EscrowEvidenceError();
|
|
1254
|
+
return record;
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
const fetchAuthorityState = async (
|
|
1258
|
+
token: string,
|
|
1259
|
+
providerType: AuthProviderType,
|
|
1260
|
+
privateKey: string,
|
|
1261
|
+
did: string,
|
|
1262
|
+
signer: DidAuthVpSigner,
|
|
1263
|
+
identity: VerifiedEnclaveIdentity,
|
|
1264
|
+
assertCurrent: () => void
|
|
1265
|
+
): Promise<EscrowAuthorityState> => {
|
|
1266
|
+
assertCurrent();
|
|
1267
|
+
if (identity.mode !== 'nitro' || !identity.authority)
|
|
1268
|
+
throw new Error('The enclave does not support verified enrollment.');
|
|
1269
|
+
const nonce = Array.from(crypto.getRandomValues(new Uint8Array(32)), byte =>
|
|
1270
|
+
byte.toString(16).padStart(2, '0')
|
|
1271
|
+
).join('');
|
|
1272
|
+
const vp = await requestFreshDidAuthVp(serverUrl, privateKey, did, signer, tenantId);
|
|
1273
|
+
assertCurrent();
|
|
1274
|
+
const response = await escrowRequest<unknown>('/authority-state', {
|
|
1275
|
+
method: 'POST',
|
|
1276
|
+
headers: buildHeaders(token, vp, tenantId),
|
|
1277
|
+
body: JSON.stringify({ authToken: token, providerType, nonce }),
|
|
1278
|
+
});
|
|
1279
|
+
const result = await verifyEscrowAuthorityState(
|
|
1280
|
+
response,
|
|
1281
|
+
nonce,
|
|
1282
|
+
tenantId ?? 'learncard',
|
|
1283
|
+
did,
|
|
1284
|
+
identity
|
|
1285
|
+
);
|
|
1286
|
+
assertCurrent();
|
|
1287
|
+
return result;
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1162
1290
|
const enrollEscrow = async (
|
|
1163
1291
|
token: string,
|
|
1164
1292
|
providerType: AuthProviderType,
|
|
@@ -1167,19 +1295,59 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1167
1295
|
shares: SSSShares,
|
|
1168
1296
|
shareVersion: number,
|
|
1169
1297
|
signDidAuthVp?: DidAuthVpSigner,
|
|
1170
|
-
verifiedKey?:
|
|
1298
|
+
verifiedKey?: VerifiedEnclaveIdentity,
|
|
1171
1299
|
pinMaterial?: { pinSalt: string; pinVerifier: string },
|
|
1172
|
-
clearPin = false
|
|
1300
|
+
clearPin = false,
|
|
1301
|
+
assertCurrent = guardedStorage().assertCurrent,
|
|
1302
|
+
// P9.1 (attack C): the device's own authenticated-account email + app
|
|
1303
|
+
// locale, bound into both the blob plaintext and the signed intent for
|
|
1304
|
+
// nitro/authority mode. Kept last (after the long-established
|
|
1305
|
+
// `assertCurrent` position) so existing positional callers that pass
|
|
1306
|
+
// `assertCurrent` explicitly are never silently shifted. Only
|
|
1307
|
+
// included in the blob plaintext when this call will resolve to
|
|
1308
|
+
// enroll/replace (see `willCarryPin` below) — Rust rejects a Carry
|
|
1309
|
+
// target blob whose plaintext already carries either field, since
|
|
1310
|
+
// the enclave always fills them in from the source blob instead.
|
|
1311
|
+
recoveryEmail = '',
|
|
1312
|
+
recoveryLanguage = ''
|
|
1173
1313
|
): Promise<void> => {
|
|
1174
1314
|
if (!config.escrow?.enabled) throw new Error('Escrow enrollment is disabled');
|
|
1175
1315
|
if (!signDidAuthVp) throw new Error('DID proof signing is required for escrow enrollment');
|
|
1176
|
-
const
|
|
1316
|
+
const identity = verifiedKey ?? (await fetchVerifiedEnclaveKey());
|
|
1317
|
+
assertCurrent();
|
|
1318
|
+
const { publicKey, keyId } = identity;
|
|
1319
|
+
const authority =
|
|
1320
|
+
config.escrow.attestation.mode === 'nitro'
|
|
1321
|
+
? await fetchAuthorityState(
|
|
1322
|
+
token,
|
|
1323
|
+
providerType,
|
|
1324
|
+
privateKey,
|
|
1325
|
+
primaryDid,
|
|
1326
|
+
signDidAuthVp,
|
|
1327
|
+
identity,
|
|
1328
|
+
assertCurrent
|
|
1329
|
+
)
|
|
1330
|
+
: undefined;
|
|
1331
|
+
// P9.1 (attack C): calls the SAME `resolveEscrowEnrollmentAction` used
|
|
1332
|
+
// by `createEscrowEnrollmentIntent` below, so the blob PLAINTEXT built
|
|
1333
|
+
// here and the signed intent built later always agree on whether this
|
|
1334
|
+
// is a carry — the enclave rejects a Carry target blob whose
|
|
1335
|
+
// plaintext carries a non-null recoveryEmail/recoveryLanguage.
|
|
1336
|
+
const bindsRecoveryContact =
|
|
1337
|
+
!authority ||
|
|
1338
|
+
resolveEscrowEnrollmentAction(
|
|
1339
|
+
authority,
|
|
1340
|
+
pinMaterial ? 'set' : clearPin ? 'clear' : 'automatic'
|
|
1341
|
+
).bindsRecoveryContact;
|
|
1177
1342
|
const envelope = await encryptEscrowBlob(
|
|
1178
1343
|
{
|
|
1179
1344
|
recoveryShare: shares.recoveryShare,
|
|
1180
1345
|
did: primaryDid,
|
|
1181
1346
|
shareVersion,
|
|
1182
1347
|
...(pinMaterial ? { pinVerifier: pinMaterial.pinVerifier } : {}),
|
|
1348
|
+
...(authority && recoveryEmail && bindsRecoveryContact
|
|
1349
|
+
? { recoveryEmail, recoveryLanguage }
|
|
1350
|
+
: {}),
|
|
1183
1351
|
},
|
|
1184
1352
|
publicKey,
|
|
1185
1353
|
keyId
|
|
@@ -1191,7 +1359,20 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1191
1359
|
signDidAuthVp,
|
|
1192
1360
|
tenantId
|
|
1193
1361
|
);
|
|
1194
|
-
|
|
1362
|
+
const signedIntent = authority
|
|
1363
|
+
? await createEscrowEnrollmentIntent({
|
|
1364
|
+
state: authority,
|
|
1365
|
+
target: envelope,
|
|
1366
|
+
shareVersion,
|
|
1367
|
+
privateKey,
|
|
1368
|
+
pinAction: pinMaterial ? 'set' : clearPin ? 'clear' : 'automatic',
|
|
1369
|
+
recoveryEmail,
|
|
1370
|
+
recoveryLanguage,
|
|
1371
|
+
})
|
|
1372
|
+
: undefined;
|
|
1373
|
+
assertCurrent();
|
|
1374
|
+
verifiedEscrowEnvelope = undefined;
|
|
1375
|
+
const response = await escrowRequest<unknown>('', {
|
|
1195
1376
|
method: 'POST',
|
|
1196
1377
|
headers: buildHeaders(token, vp, tenantId),
|
|
1197
1378
|
body: JSON.stringify({
|
|
@@ -1201,9 +1382,23 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1201
1382
|
shareVersion,
|
|
1202
1383
|
enclaveKeyId: keyId,
|
|
1203
1384
|
...(pinMaterial ? { pinSalt: pinMaterial.pinSalt } : {}),
|
|
1204
|
-
...(
|
|
1385
|
+
...((signedIntent ? signedIntent.intent.pinAction === 'clear' : clearPin)
|
|
1386
|
+
? { clearPin: true }
|
|
1387
|
+
: {}),
|
|
1388
|
+
...(signedIntent
|
|
1389
|
+
? { signedIntent, sourceEnvelope: authority?.envelope ?? null }
|
|
1390
|
+
: {}),
|
|
1205
1391
|
}),
|
|
1206
1392
|
});
|
|
1393
|
+
if (signedIntent) {
|
|
1394
|
+
const committed = await verifyEscrowEnrollmentCommit(
|
|
1395
|
+
response,
|
|
1396
|
+
signedIntent.intent,
|
|
1397
|
+
identity
|
|
1398
|
+
);
|
|
1399
|
+
assertCurrent();
|
|
1400
|
+
verifiedEscrowEnvelope = committed.envelope ?? undefined;
|
|
1401
|
+
}
|
|
1207
1402
|
};
|
|
1208
1403
|
|
|
1209
1404
|
const tryEnrollEscrow = async (...args: Parameters<typeof enrollEscrow>): Promise<void> => {
|
|
@@ -1224,11 +1419,15 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1224
1419
|
const ensureEscrowEnrollmentUnguarded = async (
|
|
1225
1420
|
self: SSSKeyDerivationStrategy,
|
|
1226
1421
|
params: Parameters<NonNullable<SSSKeyDerivationStrategy['ensureEscrowEnrollment']>>[0],
|
|
1227
|
-
storage: SSSStorageFunctions,
|
|
1228
|
-
forceRotate = false
|
|
1422
|
+
storage: SSSStorageFunctions & { assertCurrent: () => void },
|
|
1423
|
+
forceRotate = false,
|
|
1424
|
+
allowDisabledEnrollment = false
|
|
1229
1425
|
): Promise<EscrowEnrollmentResult> => {
|
|
1426
|
+
let clearPinForEnrollment = forceRotate;
|
|
1230
1427
|
if (!config.escrow?.enabled) return { enrolled: false, reason: 'disabled' };
|
|
1231
1428
|
const pin = params.options?.pin;
|
|
1429
|
+
const recoveryEmail = params.options?.recoveryEmail ?? '';
|
|
1430
|
+
const recoveryLanguage = params.options?.recoveryLanguage ?? '';
|
|
1232
1431
|
if (pin !== undefined) {
|
|
1233
1432
|
const validation = validatePin(pin);
|
|
1234
1433
|
if (!validation.ok)
|
|
@@ -1239,8 +1438,9 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1239
1438
|
);
|
|
1240
1439
|
}
|
|
1241
1440
|
const status = await self.fetchServerKeyStatus(params.token, params.providerType);
|
|
1441
|
+
storage.assertCurrent();
|
|
1242
1442
|
if (status.escrowOptedOut) return { enrolled: false, reason: 'opted-out' };
|
|
1243
|
-
|
|
1443
|
+
let currentlyEnrolled =
|
|
1244
1444
|
status.shareVersion !== null &&
|
|
1245
1445
|
status.recoveryMethods.some(
|
|
1246
1446
|
method =>
|
|
@@ -1250,6 +1450,33 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1250
1450
|
('confirmationStatus' in method &&
|
|
1251
1451
|
method.confirmationStatus === 'confirmed'))
|
|
1252
1452
|
);
|
|
1453
|
+
let verifiedKey: VerifiedEnclaveIdentity | undefined;
|
|
1454
|
+
if (config.escrow.attestation.mode === 'nitro') {
|
|
1455
|
+
if (!status.primaryDid || !params.signDidAuthVp)
|
|
1456
|
+
throw new Error('Owner proof signing is required for escrow enrollment.');
|
|
1457
|
+
verifiedKey = await fetchVerifiedEnclaveKey();
|
|
1458
|
+
const current = await fetchAuthorityState(
|
|
1459
|
+
params.token,
|
|
1460
|
+
params.providerType,
|
|
1461
|
+
params.privateKey,
|
|
1462
|
+
status.primaryDid,
|
|
1463
|
+
params.signDidAuthVp,
|
|
1464
|
+
verifiedKey,
|
|
1465
|
+
storage.assertCurrent
|
|
1466
|
+
);
|
|
1467
|
+
if (current.status === 'disabled') {
|
|
1468
|
+
verifiedEscrowEnvelope = undefined;
|
|
1469
|
+
if (!allowDisabledEnrollment) return { enrolled: false, reason: 'opted-out' };
|
|
1470
|
+
// Explicit enable without a new PIN clears the retired verifier.
|
|
1471
|
+
clearPinForEnrollment ||= current.hasPin && pin === undefined;
|
|
1472
|
+
}
|
|
1473
|
+
currentlyEnrolled =
|
|
1474
|
+
currentlyEnrolled &&
|
|
1475
|
+
current.status === 'active' &&
|
|
1476
|
+
current.shareVersion === status.shareVersion &&
|
|
1477
|
+
current.envelope?.keyId === verifiedKey.keyId;
|
|
1478
|
+
if (currentlyEnrolled) verifiedEscrowEnvelope = current.envelope ?? undefined;
|
|
1479
|
+
}
|
|
1253
1480
|
// A confirmed enrollment whose sealed blob no longer matches the active
|
|
1254
1481
|
// enclave (P6 mode migration/rollback, or a rotated enclave key) still
|
|
1255
1482
|
// needs the same rotation a missing enrollment would get — but only one
|
|
@@ -1257,11 +1484,11 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1257
1484
|
// retry on every ensureEscrowEnrollment call within the same session.
|
|
1258
1485
|
const repairingStaleBlob =
|
|
1259
1486
|
pin === undefined &&
|
|
1260
|
-
!
|
|
1487
|
+
!clearPinForEnrollment &&
|
|
1261
1488
|
currentlyEnrolled &&
|
|
1262
1489
|
!!status.escrowStale &&
|
|
1263
1490
|
!staleEscrowReenrollAttempted;
|
|
1264
|
-
if (!
|
|
1491
|
+
if (!clearPinForEnrollment && pin === undefined && currentlyEnrolled && !repairingStaleBlob)
|
|
1265
1492
|
// Automatic repair preserves current enrollment regardless of PIN status.
|
|
1266
1493
|
// The enclave can carry an existing verifier when shares rotate.
|
|
1267
1494
|
return { enrolled: true, changed: false };
|
|
@@ -1272,7 +1499,7 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1272
1499
|
}
|
|
1273
1500
|
try {
|
|
1274
1501
|
if (
|
|
1275
|
-
!
|
|
1502
|
+
!clearPinForEnrollment &&
|
|
1276
1503
|
pin === undefined &&
|
|
1277
1504
|
unenrolledRotation &&
|
|
1278
1505
|
unenrolledRotation.shareVersion === status.shareVersion &&
|
|
@@ -1289,14 +1516,18 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1289
1516
|
params.signDidAuthVp,
|
|
1290
1517
|
undefined,
|
|
1291
1518
|
undefined,
|
|
1292
|
-
retry.clearPin
|
|
1519
|
+
retry.clearPin,
|
|
1520
|
+
storage.assertCurrent,
|
|
1521
|
+
recoveryEmail,
|
|
1522
|
+
recoveryLanguage
|
|
1293
1523
|
);
|
|
1294
1524
|
unenrolledRotation = undefined;
|
|
1295
1525
|
return { enrolled: true, changed: true, shareVersion: retry.shareVersion };
|
|
1296
1526
|
}
|
|
1297
1527
|
// Verify the enclave before rotating: a bad attestation must not burn
|
|
1298
1528
|
// a share version (and, repeated, the retained auth-share history).
|
|
1299
|
-
|
|
1529
|
+
verifiedKey ??= await fetchVerifiedEnclaveKey();
|
|
1530
|
+
storage.assertCurrent();
|
|
1300
1531
|
const pinSalt = pin !== undefined ? generatePinSalt() : undefined;
|
|
1301
1532
|
const pinMaterial =
|
|
1302
1533
|
pin !== undefined && pinSalt !== undefined
|
|
@@ -1328,7 +1559,12 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1328
1559
|
};
|
|
1329
1560
|
unenrolledRotation = pinMaterial
|
|
1330
1561
|
? undefined
|
|
1331
|
-
: {
|
|
1562
|
+
: {
|
|
1563
|
+
shares,
|
|
1564
|
+
shareVersion,
|
|
1565
|
+
primaryDid: status.primaryDid,
|
|
1566
|
+
clearPin: clearPinForEnrollment,
|
|
1567
|
+
};
|
|
1332
1568
|
await enrollEscrow(
|
|
1333
1569
|
params.token,
|
|
1334
1570
|
params.providerType,
|
|
@@ -1339,7 +1575,10 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1339
1575
|
params.signDidAuthVp,
|
|
1340
1576
|
verifiedKey,
|
|
1341
1577
|
pinMaterial,
|
|
1342
|
-
|
|
1578
|
+
clearPinForEnrollment,
|
|
1579
|
+
storage.assertCurrent,
|
|
1580
|
+
recoveryEmail,
|
|
1581
|
+
recoveryLanguage
|
|
1343
1582
|
);
|
|
1344
1583
|
unenrolledRotation = undefined;
|
|
1345
1584
|
return { enrolled: true, changed: true, shareVersion };
|
|
@@ -1347,7 +1586,7 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1347
1586
|
// A background repair the caller never asked for must not surface as
|
|
1348
1587
|
// a hard failure (mirrors tryEnrollEscrow's existing swallow-and-report
|
|
1349
1588
|
// shape); an explicit not-yet-enrolled/PIN/forced rotation still throws.
|
|
1350
|
-
if (!repairingStaleBlob) throw err;
|
|
1589
|
+
if (!repairingStaleBlob || config.escrow.attestation.mode === 'nitro') throw err;
|
|
1351
1590
|
try {
|
|
1352
1591
|
if (config.onEscrowError) config.onEscrowError(err);
|
|
1353
1592
|
else console.warn('SSS: escrow enrollment failed');
|
|
@@ -1397,6 +1636,7 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1397
1636
|
// --- User scoping ---
|
|
1398
1637
|
|
|
1399
1638
|
setActiveUser(userId: string): void {
|
|
1639
|
+
verifiedEscrowEnvelope = undefined;
|
|
1400
1640
|
if (activeStorageId !== `sss-device-share:${userId}`) storageGeneration++;
|
|
1401
1641
|
activeStorageId = `sss-device-share:${userId}`;
|
|
1402
1642
|
lastServerSnapshot = null;
|
|
@@ -1458,6 +1698,7 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1458
1698
|
}
|
|
1459
1699
|
|
|
1460
1700
|
storageGeneration++;
|
|
1701
|
+
verifiedEscrowEnvelope = undefined;
|
|
1461
1702
|
await Promise.allSettled(localWrites);
|
|
1462
1703
|
await clearPendingShareCandidates(storage, storageId);
|
|
1463
1704
|
return storage.clearAllShares(storageId);
|
|
@@ -1866,8 +2107,58 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1866
2107
|
},
|
|
1867
2108
|
|
|
1868
2109
|
async disableEscrowRecovery(params): Promise<void> {
|
|
2110
|
+
const assertCurrent = guardedStorage().assertCurrent;
|
|
1869
2111
|
const status = await this.fetchServerKeyStatus(params.token, params.providerType);
|
|
1870
2112
|
if (!status.primaryDid) throw new Error('Cannot disable escrow without a primary DID');
|
|
2113
|
+
if (config.escrow?.attestation.mode === 'nitro') {
|
|
2114
|
+
const identity = await fetchVerifiedEnclaveKey();
|
|
2115
|
+
const current = await fetchAuthorityState(
|
|
2116
|
+
params.token,
|
|
2117
|
+
params.providerType,
|
|
2118
|
+
params.privateKey,
|
|
2119
|
+
status.primaryDid,
|
|
2120
|
+
params.signDidAuthVp,
|
|
2121
|
+
identity,
|
|
2122
|
+
assertCurrent
|
|
2123
|
+
);
|
|
2124
|
+
if (current.status === 'disabled') {
|
|
2125
|
+
verifiedEscrowEnvelope = undefined;
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
const signedIntent = await createEscrowEnrollmentIntent({
|
|
2129
|
+
state: current,
|
|
2130
|
+
target: null,
|
|
2131
|
+
shareVersion: current.shareVersion ?? 0,
|
|
2132
|
+
privateKey: params.privateKey,
|
|
2133
|
+
pinAction: 'disable',
|
|
2134
|
+
// Disable always nulls these server-side regardless of input.
|
|
2135
|
+
recoveryEmail: '',
|
|
2136
|
+
recoveryLanguage: '',
|
|
2137
|
+
});
|
|
2138
|
+
const vp = await requestFreshDidAuthVp(
|
|
2139
|
+
serverUrl,
|
|
2140
|
+
params.privateKey,
|
|
2141
|
+
status.primaryDid,
|
|
2142
|
+
params.signDidAuthVp,
|
|
2143
|
+
tenantId
|
|
2144
|
+
);
|
|
2145
|
+
assertCurrent();
|
|
2146
|
+
const response = await escrowRequest<unknown>('/disable', {
|
|
2147
|
+
method: 'POST',
|
|
2148
|
+
headers: buildHeaders(params.token, vp, tenantId),
|
|
2149
|
+
body: JSON.stringify({
|
|
2150
|
+
authToken: params.token,
|
|
2151
|
+
providerType: params.providerType,
|
|
2152
|
+
optOut: true,
|
|
2153
|
+
signedIntent,
|
|
2154
|
+
sourceEnvelope: current.envelope,
|
|
2155
|
+
}),
|
|
2156
|
+
});
|
|
2157
|
+
await verifyEscrowEnrollmentCommit(response, signedIntent.intent, identity);
|
|
2158
|
+
assertCurrent();
|
|
2159
|
+
verifiedEscrowEnvelope = undefined;
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
1871
2162
|
const vp = await requestFreshDidAuthVp(
|
|
1872
2163
|
serverUrl,
|
|
1873
2164
|
params.privateKey,
|
|
@@ -1909,14 +2200,28 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1909
2200
|
providerType: params.providerType,
|
|
1910
2201
|
}),
|
|
1911
2202
|
});
|
|
1912
|
-
return ensureEscrowEnrollmentUnguarded(this, params, storage);
|
|
2203
|
+
return ensureEscrowEnrollmentUnguarded(this, params, storage, false, true);
|
|
1913
2204
|
});
|
|
1914
2205
|
},
|
|
1915
2206
|
|
|
1916
2207
|
async setEscrowPin({ pin, ...params }): Promise<void> {
|
|
1917
2208
|
const storage = guardedStorage();
|
|
1918
2209
|
const result = await withRotationLock(() =>
|
|
1919
|
-
|
|
2210
|
+
// P9.1: preserve the caller's recoveryEmail/recoveryLanguage — do
|
|
2211
|
+
// not replace `params.options` wholesale, or nitro enroll/replace
|
|
2212
|
+
// loses its required recovery contact.
|
|
2213
|
+
ensureEscrowEnrollmentUnguarded(
|
|
2214
|
+
this,
|
|
2215
|
+
{
|
|
2216
|
+
...params,
|
|
2217
|
+
options: {
|
|
2218
|
+
pin,
|
|
2219
|
+
recoveryEmail: params.options?.recoveryEmail,
|
|
2220
|
+
recoveryLanguage: params.options?.recoveryLanguage,
|
|
2221
|
+
},
|
|
2222
|
+
},
|
|
2223
|
+
storage
|
|
2224
|
+
)
|
|
1920
2225
|
);
|
|
1921
2226
|
if (!result.enrolled || !result.changed) {
|
|
1922
2227
|
throw new Error('Automatic recovery is not available for this account.');
|
|
@@ -1951,8 +2256,16 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1951
2256
|
}
|
|
1952
2257
|
},
|
|
1953
2258
|
|
|
2259
|
+
getVerifiedEscrowEnvelope(): EscrowEnvelope | null {
|
|
2260
|
+
return verifiedEscrowEnvelope ? { ...verifiedEscrowEnvelope } : null;
|
|
2261
|
+
},
|
|
2262
|
+
|
|
1954
2263
|
/** Start or explicitly restart a hold with fresh client-bound resume secrets. */
|
|
1955
2264
|
async startEscrowRecovery(params): Promise<EscrowRecoveryStart> {
|
|
2265
|
+
const expectedDid = params.expectedDid;
|
|
2266
|
+
const nitro = config.escrow?.attestation.mode === 'nitro';
|
|
2267
|
+
if (nitro && (!expectedDid || (params.tenantId ?? tenantId) !== tenantId))
|
|
2268
|
+
throw new EscrowEvidenceError();
|
|
1956
2269
|
const sessionProof = params.recoverySessionToken !== undefined;
|
|
1957
2270
|
if (
|
|
1958
2271
|
sessionProof
|
|
@@ -1982,28 +2295,136 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1982
2295
|
},
|
|
1983
2296
|
params.options?.releasePolicy === 'pin'
|
|
1984
2297
|
);
|
|
1985
|
-
|
|
2298
|
+
// [D] Verify the enclave itself committed to creating THIS exact
|
|
2299
|
+
// hold — recipient key, DID and blob identity — before trusting
|
|
2300
|
+
// the response. Skipped for a reported existing hold
|
|
2301
|
+
// (`resumeToken: null`): that response isn't bound to this call's
|
|
2302
|
+
// freshly generated key pair in the first place (see
|
|
2303
|
+
// EscrowRecoveryPanel's `existingHold` display), so there is
|
|
2304
|
+
// nothing of ours to verify it against.
|
|
2305
|
+
let expectations: EscrowRecoveryExpectations | undefined;
|
|
2306
|
+
if (nitro && result.resumeToken !== null) {
|
|
2307
|
+
const identity = await fetchVerifiedEnclaveKey();
|
|
2308
|
+
if (
|
|
2309
|
+
identity.mode !== 'nitro' ||
|
|
2310
|
+
!identity.authority ||
|
|
2311
|
+
!result.holdRecord ||
|
|
2312
|
+
!expectedDid
|
|
2313
|
+
)
|
|
2314
|
+
throw new EscrowEvidenceError();
|
|
2315
|
+
const verified = await verifyEscrowHoldRecord(
|
|
2316
|
+
result.holdRecord,
|
|
2317
|
+
{
|
|
2318
|
+
tenant: tenantId ?? 'learncard',
|
|
2319
|
+
did: expectedDid,
|
|
2320
|
+
holdId: result.holdId,
|
|
2321
|
+
clientEphemeralPublicKey: pair.publicKey,
|
|
2322
|
+
blobHash: result.holdRecord.hold.blobHash,
|
|
2323
|
+
},
|
|
2324
|
+
identity
|
|
2325
|
+
);
|
|
2326
|
+
expectations = {
|
|
2327
|
+
tenant: tenantId ?? 'learncard',
|
|
2328
|
+
did: expectedDid,
|
|
2329
|
+
holdId: result.holdId,
|
|
2330
|
+
clientEphemeralPublicKey: pair.publicKey,
|
|
2331
|
+
blobHash: result.holdRecord.hold.blobHash,
|
|
2332
|
+
authorityGeneration: verified.authorityGeneration,
|
|
2333
|
+
holdDigest: verified.holdDigest,
|
|
2334
|
+
ledgerPublicKey: identity.authority.ledgerPublicKey,
|
|
2335
|
+
keyId: verified.receipt.keyId,
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
// Never accept a server-supplied expectations object, even in software mode.
|
|
2339
|
+
return { ...result, clientEphemeralPrivateKey: pair.privateKey, expectations };
|
|
1986
2340
|
},
|
|
1987
2341
|
|
|
1988
2342
|
/** Read hold status; secrets (auth or resume token) never travel in a URL. */
|
|
1989
2343
|
async getEscrowRecoveryStatus(params): Promise<EscrowHoldStatus | null> {
|
|
1990
|
-
const
|
|
1991
|
-
'holdId' in params
|
|
1992
|
-
?
|
|
1993
|
-
:
|
|
1994
|
-
);
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2344
|
+
const expected =
|
|
2345
|
+
config.escrow?.attestation.mode === 'nitro' && 'holdId' in params
|
|
2346
|
+
? recoveryExpectations(params.holdId, params.expectations)
|
|
2347
|
+
: undefined;
|
|
2348
|
+
const status = await fetchEscrowHoldStatus(params);
|
|
2349
|
+
if (expected && status)
|
|
2350
|
+
await verifyPendingHold(status, expected, await fetchVerifiedEnclaveKey());
|
|
2351
|
+
return status;
|
|
2352
|
+
},
|
|
2353
|
+
|
|
2354
|
+
/** Submit the confirmation code emailed by the enclave for an authority-mode hold. */
|
|
2355
|
+
async confirmEscrowRecovery(params): Promise<{ success: true }> {
|
|
2356
|
+
// [D] Fetch and verify the hold record this code is being
|
|
2357
|
+
// confirmed against (the same hold `startEscrowRecovery` already
|
|
2358
|
+
// verified at creation) BEFORE submitting the code, then verify
|
|
2359
|
+
// the confirmation result commits to that SAME hold — a wrong
|
|
2360
|
+
// recipient key, hold id/digest, DID, unsigned/forged record, or
|
|
2361
|
+
// a retry snapshot for a different hold all fail closed here.
|
|
2362
|
+
let pending:
|
|
2363
|
+
| {
|
|
2364
|
+
identity: VerifiedEnclaveIdentity;
|
|
2365
|
+
holdRecord: EscrowHoldRecordLike;
|
|
2366
|
+
expected: EscrowRecoveryExpectations;
|
|
2367
|
+
}
|
|
2368
|
+
| undefined;
|
|
2369
|
+
if (config.escrow?.attestation.mode === 'nitro') {
|
|
2370
|
+
const expected = recoveryExpectations(params.holdId, params.expectations);
|
|
2371
|
+
if ((params.tenantId ?? tenantId) !== tenantId) throw new EscrowEvidenceError();
|
|
2372
|
+
const identity = await fetchVerifiedEnclaveKey();
|
|
2373
|
+
const status = await fetchEscrowHoldStatus({
|
|
2374
|
+
holdId: params.holdId,
|
|
2375
|
+
resumeToken: params.resumeToken,
|
|
2376
|
+
});
|
|
2377
|
+
if (!status?.holdRecord) {
|
|
2378
|
+
throw new EscrowEvidenceError();
|
|
2004
2379
|
}
|
|
2005
|
-
|
|
2006
|
-
|
|
2380
|
+
const holdRecord = await verifyPendingHold(status, expected, identity);
|
|
2381
|
+
pending = { identity, holdRecord, expected };
|
|
2382
|
+
}
|
|
2383
|
+
const confirm = () =>
|
|
2384
|
+
escrowRequest<{ success: true; receipt?: unknown }>('/confirm', {
|
|
2385
|
+
method: 'POST',
|
|
2386
|
+
headers: buildHeaders('', undefined, params.tenantId ?? tenantId),
|
|
2387
|
+
body: JSON.stringify({
|
|
2388
|
+
holdId: params.holdId,
|
|
2389
|
+
resumeToken: params.resumeToken,
|
|
2390
|
+
code: params.code,
|
|
2391
|
+
}),
|
|
2392
|
+
});
|
|
2393
|
+
const result = await confirm();
|
|
2394
|
+
if (pending) {
|
|
2395
|
+
if (!result.receipt) {
|
|
2396
|
+
throw new EscrowEvidenceError();
|
|
2397
|
+
}
|
|
2398
|
+
const receipt = await verifyEscrowHoldConfirmation(
|
|
2399
|
+
result.receipt,
|
|
2400
|
+
pending.holdRecord,
|
|
2401
|
+
pending.expected,
|
|
2402
|
+
pending.identity
|
|
2403
|
+
);
|
|
2404
|
+
// A commit's secret-keyed requestDigest cannot be checked by the
|
|
2405
|
+
// client. Require the idempotent readback snapshot, which exposes
|
|
2406
|
+
// the exact hold digest without exposing an offline code oracle.
|
|
2407
|
+
if (receipt.kind === 'commit') {
|
|
2408
|
+
const readback = await confirm();
|
|
2409
|
+
const snapshot = await verifyEscrowHoldConfirmation(
|
|
2410
|
+
readback.receipt,
|
|
2411
|
+
pending.holdRecord,
|
|
2412
|
+
pending.expected,
|
|
2413
|
+
pending.identity
|
|
2414
|
+
);
|
|
2415
|
+
if (snapshot.kind !== 'snapshot') throw new EscrowEvidenceError();
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
return { success: true };
|
|
2419
|
+
},
|
|
2420
|
+
|
|
2421
|
+
/** Request a fresh confirmation code for a hold awaiting confirmation. */
|
|
2422
|
+
async resendEscrowRecoveryCode(params): Promise<{ success: true }> {
|
|
2423
|
+
return escrowRequest<{ success: true }>('/resend', {
|
|
2424
|
+
method: 'POST',
|
|
2425
|
+
headers: buildHeaders('', undefined, params.tenantId ?? tenantId),
|
|
2426
|
+
body: JSON.stringify({ holdId: params.holdId, resumeToken: params.resumeToken }),
|
|
2427
|
+
});
|
|
2007
2428
|
},
|
|
2008
2429
|
|
|
2009
2430
|
/** Cancel a pending hold with a fresh DID-owner proof. */
|
|
@@ -2048,9 +2469,23 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
2048
2469
|
? await this.startEscrowRecovery!({
|
|
2049
2470
|
token,
|
|
2050
2471
|
providerType,
|
|
2472
|
+
expectedDid: input.expectedDid,
|
|
2051
2473
|
options: { releasePolicy: 'pin' },
|
|
2052
2474
|
})
|
|
2053
2475
|
: input;
|
|
2476
|
+
const expected =
|
|
2477
|
+
config.escrow?.attestation.mode === 'nitro'
|
|
2478
|
+
? recoveryExpectations(start.holdId, start.expectations)
|
|
2479
|
+
: undefined;
|
|
2480
|
+
if (expected) {
|
|
2481
|
+
if (!start.resumeToken) throw new EscrowEvidenceError();
|
|
2482
|
+
const status = await fetchEscrowHoldStatus({
|
|
2483
|
+
holdId: start.holdId,
|
|
2484
|
+
resumeToken: start.resumeToken,
|
|
2485
|
+
});
|
|
2486
|
+
if (!status) throw new EscrowEvidenceError();
|
|
2487
|
+
await verifyPendingHold(status, expected, await fetchVerifiedEnclaveKey());
|
|
2488
|
+
}
|
|
2054
2489
|
let pinProof: string | undefined;
|
|
2055
2490
|
if (input.method === 'escrow-pin') {
|
|
2056
2491
|
if (!('pinSalt' in start) || !start.pinSalt || !start.resumeToken)
|
|
@@ -2076,6 +2511,11 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
2076
2511
|
material.sealedShare,
|
|
2077
2512
|
start.clientEphemeralPrivateKey
|
|
2078
2513
|
);
|
|
2514
|
+
if (
|
|
2515
|
+
expected &&
|
|
2516
|
+
(plaintext.did !== expected.did || plaintext.holdId !== expected.holdId)
|
|
2517
|
+
)
|
|
2518
|
+
throw new EscrowEvidenceError();
|
|
2079
2519
|
if (
|
|
2080
2520
|
plaintext.holdId !== start.holdId ||
|
|
2081
2521
|
plaintext.did !== material.primaryDid ||
|
|
@@ -3113,6 +3553,7 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
3113
3553
|
|
|
3114
3554
|
async cleanup(): Promise<void> {
|
|
3115
3555
|
storageGeneration++;
|
|
3556
|
+
verifiedEscrowEnvelope = undefined;
|
|
3116
3557
|
await Promise.allSettled(localWrites);
|
|
3117
3558
|
},
|
|
3118
3559
|
};
|