@learncard/sss-key-manager 0.2.0 → 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/README.md +13 -0
- package/dist/escrow-attestation.d.ts +2 -5
- 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 +34 -0
- package/dist/escrow-nitro-attestation.d.ts.map +1 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/sss-key-manager.cjs.development.js +4478 -424
- package/dist/sss-key-manager.cjs.development.js.map +4 -4
- package/dist/sss-key-manager.cjs.production.min.js +37 -32
- package/dist/sss-key-manager.cjs.production.min.js.map +4 -4
- package/dist/sss-key-manager.esm.js +1813 -265
- package/dist/sss-key-manager.esm.js.map +4 -4
- package/dist/sss-strategy.d.ts.map +1 -1
- package/dist/types.d.ts +31 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -3
- 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__/escrow-vectors-rust.json +82 -0
- package/src/__fixtures__/escrow-vectors.json +103 -0
- package/src/__fixtures__/nitro-attestation/bad-signature.cbor.b64 +1 -0
- package/src/__fixtures__/nitro-attestation/debug-zero-pcrs.cbor.b64 +1 -0
- package/src/__fixtures__/nitro-attestation/legacy-user-data.cbor.b64 +1 -0
- package/src/__fixtures__/nitro-attestation/manifest.json +60 -0
- package/src/__fixtures__/nitro-attestation/nonce-mismatch.cbor.b64 +1 -0
- package/src/__fixtures__/nitro-attestation/root.pem +12 -0
- package/src/__fixtures__/nitro-attestation/stale-timestamp.cbor.b64 +1 -0
- package/src/__fixtures__/nitro-attestation/tampered-previous-key-ids.cbor.b64 +1 -0
- package/src/__fixtures__/nitro-attestation/truncated-chain.cbor.b64 +1 -0
- package/src/__fixtures__/nitro-attestation/valid.cbor.b64 +1 -0
- package/src/__fixtures__/nitro-attestation/wrong-pcr0.cbor.b64 +1 -0
- package/src/__fixtures__/nitro-attestation/wrong-root.cbor.b64 +1 -0
- package/src/__fixtures__/nitro-attestation-real/staging-nsm-document.json +16 -0
- package/src/escrow-attestation.test.ts +7 -2
- package/src/escrow-attestation.ts +72 -10
- 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 +276 -0
- package/src/escrow-nitro-attestation.ts +317 -0
- package/src/escrow-vectors.test.ts +198 -0
- package/src/index.ts +35 -1
- package/src/sss-authority.test.ts +889 -0
- package/src/sss-session-writes.test.ts +2 -2
- package/src/sss-strategy.test.ts +95 -2
- package/src/sss-strategy.ts +596 -115
- package/src/types.ts +34 -4
|
@@ -61,7 +61,7 @@ describe('session-scoped enrollment writes', () => {
|
|
|
61
61
|
vi.stubGlobal(
|
|
62
62
|
'fetch',
|
|
63
63
|
vi.fn(async (url: string) => {
|
|
64
|
-
if (url.
|
|
64
|
+
if (url.includes('/attestation?nonce=')) {
|
|
65
65
|
started.resolve();
|
|
66
66
|
return attestation.promise;
|
|
67
67
|
}
|
|
@@ -91,7 +91,7 @@ describe('session-scoped enrollment writes', () => {
|
|
|
91
91
|
return authWrite.promise;
|
|
92
92
|
}
|
|
93
93
|
if (url.endsWith('/challenge')) return response({ challenge: 'nonce' });
|
|
94
|
-
if (url.
|
|
94
|
+
if (url.includes('/attestation?nonce=')) return response({ attestation: {} });
|
|
95
95
|
return response(keyStatus);
|
|
96
96
|
})
|
|
97
97
|
);
|
package/src/sss-strategy.test.ts
CHANGED
|
@@ -160,6 +160,7 @@ describe('escrow strategy', () => {
|
|
|
160
160
|
let releasePolicy: 'hold' | 'pin';
|
|
161
161
|
let consumed: boolean;
|
|
162
162
|
let burned: string[];
|
|
163
|
+
let escrowStale: 'mode-mismatch' | 'key-rotated' | undefined;
|
|
163
164
|
const hold = () => ({
|
|
164
165
|
holdId: holdNumber <= 1 ? 'escrow-hold' : `escrow-hold-${holdNumber}`,
|
|
165
166
|
status: cancelled ? 'cancelled' : 'pending',
|
|
@@ -190,6 +191,7 @@ describe('escrow strategy', () => {
|
|
|
190
191
|
releasePolicy = 'hold';
|
|
191
192
|
consumed = false;
|
|
192
193
|
burned = [];
|
|
194
|
+
escrowStale = undefined;
|
|
193
195
|
config = {
|
|
194
196
|
serverUrl: 'https://test.example/api',
|
|
195
197
|
storage,
|
|
@@ -225,6 +227,7 @@ describe('escrow strategy', () => {
|
|
|
225
227
|
attemptsRemaining,
|
|
226
228
|
...(pinSalt ? { salt: pinSalt } : {}),
|
|
227
229
|
},
|
|
230
|
+
...(escrowStale ? { escrowStale } : {}),
|
|
228
231
|
});
|
|
229
232
|
}
|
|
230
233
|
if (path === '/keys/recovery' || path === '/keys/recovery/confirm')
|
|
@@ -266,6 +269,7 @@ describe('escrow strategy', () => {
|
|
|
266
269
|
blob = await decryptEscrowBlob(body.envelope, enclaveKeys.privateKey);
|
|
267
270
|
pinSalt = body.pinSalt;
|
|
268
271
|
attemptsRemaining = 10;
|
|
272
|
+
escrowStale = undefined;
|
|
269
273
|
expect(blob.did).toBe(did);
|
|
270
274
|
expect(blob.shareVersion).toBe(version);
|
|
271
275
|
expect(
|
|
@@ -480,7 +484,7 @@ describe('escrow strategy', () => {
|
|
|
480
484
|
});
|
|
481
485
|
methods[0].shareVersion = 1;
|
|
482
486
|
expect(await strategy.getEscrowEnrollmentState!(params)).toMatchObject({
|
|
483
|
-
state: '
|
|
487
|
+
state: 'stale',
|
|
484
488
|
});
|
|
485
489
|
methods[0].shareVersion = version;
|
|
486
490
|
methods[0].confirmedAt = undefined;
|
|
@@ -543,7 +547,7 @@ describe('escrow strategy', () => {
|
|
|
543
547
|
await strategy.setupRecoveryMethod!({ ...params, input: { method: 'phrase' } });
|
|
544
548
|
expect(config.onEscrowError).toHaveBeenCalledWith(
|
|
545
549
|
expect.objectContaining({
|
|
546
|
-
message: '
|
|
550
|
+
message: 'Escrow attestation mode mismatch',
|
|
547
551
|
})
|
|
548
552
|
);
|
|
549
553
|
});
|
|
@@ -571,6 +575,77 @@ describe('escrow strategy', () => {
|
|
|
571
575
|
});
|
|
572
576
|
expect(calls).toHaveLength(0);
|
|
573
577
|
});
|
|
578
|
+
it('getEscrowEnrollmentState reports stale with a reason when the server flags the blob', async () => {
|
|
579
|
+
await strategy.ensureEscrowEnrollment!(params);
|
|
580
|
+
escrowStale = 'mode-mismatch';
|
|
581
|
+
expect(await strategy.getEscrowEnrollmentState!(params)).toMatchObject({
|
|
582
|
+
state: 'stale',
|
|
583
|
+
staleReason: 'mode-mismatch',
|
|
584
|
+
});
|
|
585
|
+
escrowStale = 'key-rotated';
|
|
586
|
+
expect(await strategy.getEscrowEnrollmentState!(params)).toMatchObject({
|
|
587
|
+
state: 'stale',
|
|
588
|
+
staleReason: 'key-rotated',
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
it('does not trigger the stale-repair path when the server reports no staleness', async () => {
|
|
592
|
+
await strategy.ensureEscrowEnrollment!(params);
|
|
593
|
+
calls = [];
|
|
594
|
+
await expect(strategy.ensureEscrowEnrollment!(params)).resolves.toEqual({
|
|
595
|
+
enrolled: true,
|
|
596
|
+
changed: false,
|
|
597
|
+
});
|
|
598
|
+
expect(calls.some(call => call.path === '/keys/escrow/attestation')).toBe(false);
|
|
599
|
+
});
|
|
600
|
+
it('rotates a stale enrollment through the normal rotation path, at most once per session', async () => {
|
|
601
|
+
await strategy.ensureEscrowEnrollment!(params);
|
|
602
|
+
escrowStale = 'mode-mismatch';
|
|
603
|
+
await expect(strategy.ensureEscrowEnrollment!(params)).resolves.toEqual({
|
|
604
|
+
enrolled: true,
|
|
605
|
+
changed: true,
|
|
606
|
+
shareVersion: 3,
|
|
607
|
+
});
|
|
608
|
+
expect(version).toBe(3);
|
|
609
|
+
expect(blob?.shareVersion).toBe(3);
|
|
610
|
+
// A real re-enroll clears server-side staleness (the mock's '/keys/escrow'
|
|
611
|
+
// handler already reset it); force it stale again so this assertion proves
|
|
612
|
+
// the session-scoped rate limit — not the mock's own healing — stops the retry.
|
|
613
|
+
escrowStale = 'mode-mismatch';
|
|
614
|
+
calls = [];
|
|
615
|
+
await expect(strategy.ensureEscrowEnrollment!(params)).resolves.toEqual({
|
|
616
|
+
enrolled: true,
|
|
617
|
+
changed: false,
|
|
618
|
+
});
|
|
619
|
+
expect(calls.some(call => call.path === '/keys/escrow/attestation')).toBe(false);
|
|
620
|
+
expect(version).toBe(3);
|
|
621
|
+
// A fresh strategy instance (the next app session) attempts the repair again.
|
|
622
|
+
const nextSession = createSSSStrategy(config);
|
|
623
|
+
await expect(nextSession.ensureEscrowEnrollment!(params)).resolves.toEqual({
|
|
624
|
+
enrolled: true,
|
|
625
|
+
changed: true,
|
|
626
|
+
shareVersion: 4,
|
|
627
|
+
});
|
|
628
|
+
expect(version).toBe(4);
|
|
629
|
+
});
|
|
630
|
+
it('swallows a failed automatic stale-blob repair instead of throwing to the caller', async () => {
|
|
631
|
+
await strategy.ensureEscrowEnrollment!(params);
|
|
632
|
+
escrowStale = 'key-rotated';
|
|
633
|
+
attestationFailure = true;
|
|
634
|
+
await expect(strategy.ensureEscrowEnrollment!(params)).resolves.toEqual({
|
|
635
|
+
enrolled: true,
|
|
636
|
+
changed: false,
|
|
637
|
+
});
|
|
638
|
+
expect(version).toBe(2);
|
|
639
|
+
expect(config.onEscrowError).toHaveBeenCalledOnce();
|
|
640
|
+
vi.mocked(config.onEscrowError!).mockClear();
|
|
641
|
+
// Still rate-limited to one attempt this session, even though it failed.
|
|
642
|
+
await expect(strategy.ensureEscrowEnrollment!(params)).resolves.toEqual({
|
|
643
|
+
enrolled: true,
|
|
644
|
+
changed: false,
|
|
645
|
+
});
|
|
646
|
+
expect(config.onEscrowError).not.toHaveBeenCalled();
|
|
647
|
+
expect(version).toBe(2);
|
|
648
|
+
});
|
|
574
649
|
it('requires DID verification before attempting PIN recovery', async () => {
|
|
575
650
|
await expect(
|
|
576
651
|
strategy.executeRecovery({
|
|
@@ -585,6 +660,24 @@ describe('escrow strategy', () => {
|
|
|
585
660
|
expect(storage.clearAllShares).not.toHaveBeenCalled();
|
|
586
661
|
expect(strategy.hasPendingIdentityRecovery!()).toBe(false);
|
|
587
662
|
});
|
|
663
|
+
it.each(['set', 'clear'] as const)(
|
|
664
|
+
'rejects explicit %s PIN on a stale account when the enclave is unreachable',
|
|
665
|
+
async action => {
|
|
666
|
+
await strategy.ensureEscrowEnrollment!(params);
|
|
667
|
+
escrowStale = 'key-rotated';
|
|
668
|
+
attestationFailure = true;
|
|
669
|
+
await expect(
|
|
670
|
+
action === 'set'
|
|
671
|
+
? strategy.setEscrowPin!({ ...params, pin: '135790' })
|
|
672
|
+
: strategy.clearEscrowPin!(params)
|
|
673
|
+
).rejects.toThrow();
|
|
674
|
+
expect(version).toBe(2);
|
|
675
|
+
expect(config.onEscrowError).not.toHaveBeenCalled();
|
|
676
|
+
// Explicit actions must not consume the automatic repair attempt.
|
|
677
|
+
await strategy.ensureEscrowEnrollment!(params);
|
|
678
|
+
expect(config.onEscrowError).toHaveBeenCalledOnce();
|
|
679
|
+
}
|
|
680
|
+
);
|
|
588
681
|
const recoverPin = (pin = '135790') =>
|
|
589
682
|
strategy.executeRecovery({
|
|
590
683
|
...params,
|