@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.
Files changed (63) hide show
  1. package/README.md +13 -0
  2. package/dist/escrow-attestation.d.ts +2 -5
  3. package/dist/escrow-attestation.d.ts.map +1 -1
  4. package/dist/escrow-authority.d.ts +112 -0
  5. package/dist/escrow-authority.d.ts.map +1 -0
  6. package/dist/escrow-authority.types.d.ts +51 -0
  7. package/dist/escrow-authority.types.d.ts.map +1 -0
  8. package/dist/escrow-crypto.d.ts +9 -1
  9. package/dist/escrow-crypto.d.ts.map +1 -1
  10. package/dist/escrow-enrollment-intent.d.ts +13 -0
  11. package/dist/escrow-enrollment-intent.d.ts.map +1 -0
  12. package/dist/escrow-enrollment-intent.types.d.ts +28 -0
  13. package/dist/escrow-enrollment-intent.types.d.ts.map +1 -0
  14. package/dist/escrow-nitro-attestation.d.ts +34 -0
  15. package/dist/escrow-nitro-attestation.d.ts.map +1 -0
  16. package/dist/index.d.ts +11 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/sss-key-manager.cjs.development.js +4478 -424
  19. package/dist/sss-key-manager.cjs.development.js.map +4 -4
  20. package/dist/sss-key-manager.cjs.production.min.js +37 -32
  21. package/dist/sss-key-manager.cjs.production.min.js.map +4 -4
  22. package/dist/sss-key-manager.esm.js +1813 -265
  23. package/dist/sss-key-manager.esm.js.map +4 -4
  24. package/dist/sss-strategy.d.ts.map +1 -1
  25. package/dist/types.d.ts +31 -5
  26. package/dist/types.d.ts.map +1 -1
  27. package/package.json +7 -3
  28. package/src/__fixtures__/escrow-authority-v1.json +96 -0
  29. package/src/__fixtures__/escrow-carry-contact-contract-v1.json +33 -0
  30. package/src/__fixtures__/escrow-enrollment-intent-v1.json +26 -0
  31. package/src/__fixtures__/escrow-vectors-rust.json +82 -0
  32. package/src/__fixtures__/escrow-vectors.json +103 -0
  33. package/src/__fixtures__/nitro-attestation/bad-signature.cbor.b64 +1 -0
  34. package/src/__fixtures__/nitro-attestation/debug-zero-pcrs.cbor.b64 +1 -0
  35. package/src/__fixtures__/nitro-attestation/legacy-user-data.cbor.b64 +1 -0
  36. package/src/__fixtures__/nitro-attestation/manifest.json +60 -0
  37. package/src/__fixtures__/nitro-attestation/nonce-mismatch.cbor.b64 +1 -0
  38. package/src/__fixtures__/nitro-attestation/root.pem +12 -0
  39. package/src/__fixtures__/nitro-attestation/stale-timestamp.cbor.b64 +1 -0
  40. package/src/__fixtures__/nitro-attestation/tampered-previous-key-ids.cbor.b64 +1 -0
  41. package/src/__fixtures__/nitro-attestation/truncated-chain.cbor.b64 +1 -0
  42. package/src/__fixtures__/nitro-attestation/valid.cbor.b64 +1 -0
  43. package/src/__fixtures__/nitro-attestation/wrong-pcr0.cbor.b64 +1 -0
  44. package/src/__fixtures__/nitro-attestation/wrong-root.cbor.b64 +1 -0
  45. package/src/__fixtures__/nitro-attestation-real/staging-nsm-document.json +16 -0
  46. package/src/escrow-attestation.test.ts +7 -2
  47. package/src/escrow-attestation.ts +72 -10
  48. package/src/escrow-authority.test.ts +643 -0
  49. package/src/escrow-authority.ts +597 -0
  50. package/src/escrow-authority.types.ts +56 -0
  51. package/src/escrow-crypto.ts +39 -1
  52. package/src/escrow-enrollment-intent.test.ts +224 -0
  53. package/src/escrow-enrollment-intent.ts +236 -0
  54. package/src/escrow-enrollment-intent.types.ts +28 -0
  55. package/src/escrow-nitro-attestation.test.ts +276 -0
  56. package/src/escrow-nitro-attestation.ts +317 -0
  57. package/src/escrow-vectors.test.ts +198 -0
  58. package/src/index.ts +35 -1
  59. package/src/sss-authority.test.ts +889 -0
  60. package/src/sss-session-writes.test.ts +2 -2
  61. package/src/sss-strategy.test.ts +95 -2
  62. package/src/sss-strategy.ts +596 -115
  63. 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.endsWith('/attestation')) {
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.endsWith('/attestation')) return response({ attestation: {} });
94
+ if (url.includes('/attestation?nonce=')) return response({ attestation: {} });
95
95
  return response(keyStatus);
96
96
  })
97
97
  );
@@ -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: 'not-enrolled',
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: 'Nitro attestation verification is not implemented yet',
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,