@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
@@ -0,0 +1,198 @@
1
+ // @vitest-environment node
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { expect, it } from 'vitest';
5
+ import { base64ToBuffer } from './crypto';
6
+
7
+ import {
8
+ decryptEscrowBlob,
9
+ ESCROW_BLOB_INFO,
10
+ ESCROW_RELEASE_INFO,
11
+ encryptEscrowBlob,
12
+ generateEscrowKeyPair,
13
+ openEscrowRelease,
14
+ sealEscrowRelease,
15
+ } from './escrow-crypto';
16
+ import type { EscrowBlobPlaintext, EscrowEnvelope, EscrowReleasePlaintext } from './escrow-crypto';
17
+
18
+ type KeyPair = Awaited<ReturnType<typeof generateEscrowKeyPair>>;
19
+ interface Vectors {
20
+ // Public test-only keys, never used for real recovery data.
21
+ enclave: KeyPair;
22
+ client: KeyPair;
23
+ blobs: { plaintext: EscrowBlobPlaintext; envelope: EscrowEnvelope }[];
24
+ releases: { plaintext: EscrowReleasePlaintext; envelope: EscrowEnvelope }[];
25
+ }
26
+
27
+ it('decrypts shared TS/Rust escrow vectors (ESCROW_WRITE_VECTORS=1 regenerates)', async () => {
28
+ const directory = fileURLToPath(new URL('./__fixtures__/', import.meta.url));
29
+ const path = fileURLToPath(new URL('./__fixtures__/escrow-vectors.json', import.meta.url));
30
+
31
+ if (process.env.ESCROW_WRITE_VECTORS === '1') {
32
+ const enclave = await generateEscrowKeyPair();
33
+ const client = await generateEscrowKeyPair();
34
+ const inputs = [
35
+ { recoveryShare: 'ABCDE', did: 'did:example:legacy', shareVersion: 1 },
36
+ {
37
+ recoveryShare: `0001${'AB'.repeat(48)}`,
38
+ did: 'did:example:pin',
39
+ shareVersion: 42,
40
+ pinVerifier: 'CD'.repeat(32),
41
+ },
42
+ {
43
+ recoveryShare: 'ef'.repeat(2048),
44
+ did: `did:${'x'.repeat(2042)}😀`,
45
+ shareVersion: 9_007_199_254_740_992,
46
+ },
47
+ ];
48
+ const blobs = await Promise.all(
49
+ inputs.map(async (input, index) => {
50
+ const envelope = await encryptEscrowBlob(
51
+ input,
52
+ enclave.publicKey,
53
+ `test-key.${index}`
54
+ );
55
+ return {
56
+ plaintext: await decryptEscrowBlob(envelope, enclave.privateKey),
57
+ envelope,
58
+ };
59
+ })
60
+ );
61
+ const releases = await Promise.all(
62
+ inputs.slice(0, 2).map(async (input, index) => {
63
+ const envelope = await sealEscrowRelease(
64
+ { ...input, holdId: `hold-${index}.test_1` },
65
+ client.publicKey
66
+ );
67
+ return {
68
+ plaintext: await openEscrowRelease(envelope, client.privateKey),
69
+ envelope,
70
+ };
71
+ })
72
+ );
73
+ const vectors: Vectors = { enclave, client, blobs, releases };
74
+ await mkdir(directory, { recursive: true });
75
+ await writeFile(path, `${JSON.stringify(vectors, null, 4)}\n`);
76
+ }
77
+
78
+ const vectors: Vectors = JSON.parse(await readFile(path, 'utf8'));
79
+ expect(vectors.blobs).toHaveLength(3);
80
+ expect(vectors.releases).toHaveLength(2);
81
+ for (const { plaintext, envelope } of vectors.blobs) {
82
+ await expect(decryptEscrowBlob(envelope, vectors.enclave.privateKey)).resolves.toEqual(
83
+ plaintext
84
+ );
85
+ }
86
+ for (const { plaintext, envelope } of vectors.releases) {
87
+ await expect(openEscrowRelease(envelope, vectors.client.privateKey)).resolves.toEqual(
88
+ plaintext
89
+ );
90
+ }
91
+ });
92
+
93
+ // Independent WebCrypto decryption exposes raw JSON before the production validator
94
+ // normalizes it, so a numeric 1.0 cannot silently satisfy the integral-token assertion.
95
+ const decryptRawJson = async (
96
+ envelope: EscrowEnvelope,
97
+ privateKey: string,
98
+ info: string
99
+ ): Promise<string> => {
100
+ const recipient = await crypto.subtle.importKey(
101
+ 'pkcs8',
102
+ base64ToBuffer(privateKey),
103
+ { name: 'ECDH', namedCurve: 'P-256' },
104
+ false,
105
+ ['deriveBits']
106
+ );
107
+ const ephemeral = await crypto.subtle.importKey(
108
+ 'raw',
109
+ base64ToBuffer(envelope.ephemeralPublicKey),
110
+ { name: 'ECDH', namedCurve: 'P-256' },
111
+ false,
112
+ []
113
+ );
114
+ const shared = await crypto.subtle.deriveBits(
115
+ { name: 'ECDH', public: ephemeral },
116
+ recipient,
117
+ 256
118
+ );
119
+ const material = await crypto.subtle.importKey('raw', shared, 'HKDF', false, ['deriveKey']);
120
+ const key = await crypto.subtle.deriveKey(
121
+ {
122
+ name: 'HKDF',
123
+ hash: 'SHA-256',
124
+ salt: base64ToBuffer(envelope.salt),
125
+ info: new TextEncoder().encode(info),
126
+ },
127
+ material,
128
+ { name: 'AES-GCM', length: 256 },
129
+ false,
130
+ ['decrypt']
131
+ );
132
+ const plaintext = await crypto.subtle.decrypt(
133
+ {
134
+ name: 'AES-GCM',
135
+ iv: base64ToBuffer(envelope.iv),
136
+ additionalData: new TextEncoder().encode(
137
+ `${envelope.version}|${envelope.algorithm}|${envelope.keyId}`
138
+ ),
139
+ },
140
+ key,
141
+ base64ToBuffer(envelope.ciphertext)
142
+ );
143
+ return new TextDecoder().decode(plaintext);
144
+ };
145
+
146
+ it('opens Rust blobs and releases in WebCrypto and emits integral JSON share versions', async () => {
147
+ const ts: Vectors = JSON.parse(
148
+ await readFile(
149
+ fileURLToPath(new URL('./__fixtures__/escrow-vectors.json', import.meta.url)),
150
+ 'utf8'
151
+ )
152
+ );
153
+ const rust: Omit<Vectors, 'client'> = JSON.parse(
154
+ await readFile(
155
+ fileURLToPath(new URL('./__fixtures__/escrow-vectors-rust.json', import.meta.url)),
156
+ 'utf8'
157
+ )
158
+ );
159
+ expect(rust.blobs).toHaveLength(2);
160
+ expect(rust.releases).toHaveLength(2);
161
+
162
+ // Import Rust SPKI as well as PKCS#8, and verify the two exports describe one key.
163
+ const publicKey = await crypto.subtle.importKey(
164
+ 'spki',
165
+ base64ToBuffer(rust.enclave.publicKey),
166
+ { name: 'ECDH', namedCurve: 'P-256' },
167
+ true,
168
+ []
169
+ );
170
+ const privateKey = await crypto.subtle.importKey(
171
+ 'pkcs8',
172
+ base64ToBuffer(rust.enclave.privateKey),
173
+ { name: 'ECDH', namedCurve: 'P-256' },
174
+ true,
175
+ ['deriveBits']
176
+ );
177
+ const publicJwk = await crypto.subtle.exportKey('jwk', publicKey);
178
+ expect(await crypto.subtle.exportKey('jwk', privateKey)).toMatchObject({
179
+ crv: 'P-256',
180
+ x: publicJwk.x,
181
+ y: publicJwk.y,
182
+ });
183
+
184
+ for (const { plaintext, envelope } of rust.blobs) {
185
+ await expect(decryptEscrowBlob(envelope, rust.enclave.privateKey)).resolves.toEqual(
186
+ plaintext
187
+ );
188
+ const raw = await decryptRawJson(envelope, rust.enclave.privateKey, ESCROW_BLOB_INFO);
189
+ expect(raw).toMatch(new RegExp(`"shareVersion":${plaintext.shareVersion}[,}]`));
190
+ expect(JSON.parse(raw)).toEqual(plaintext);
191
+ }
192
+ for (const { plaintext, envelope } of rust.releases) {
193
+ await expect(openEscrowRelease(envelope, ts.client.privateKey)).resolves.toEqual(plaintext);
194
+ const raw = await decryptRawJson(envelope, ts.client.privateKey, ESCROW_RELEASE_INFO);
195
+ expect(raw).toMatch(new RegExp(`"shareVersion":${plaintext.shareVersion}[,}]`));
196
+ expect(JSON.parse(raw)).toEqual(plaintext);
197
+ }
198
+ });
package/src/index.ts CHANGED
@@ -76,9 +76,43 @@ export {
76
76
  } from './escrow-crypto';
77
77
 
78
78
  export type { EscrowEnvelope, EscrowBlobPlaintext, EscrowReleasePlaintext } from './escrow-crypto';
79
+ export {
80
+ encodeEscrowEnrollmentIntent,
81
+ signEscrowEnrollmentIntent,
82
+ getEscrowBlobIdentity,
83
+ ESCROW_ENROLLMENT_INTENT_DOMAIN,
84
+ } from './escrow-enrollment-intent';
85
+ export type {
86
+ EscrowEnrollmentIntent,
87
+ SignedEscrowEnrollmentIntent,
88
+ } from './escrow-enrollment-intent.types';
79
89
  export { verifyEnclaveAttestation } from './escrow-attestation';
90
+ export { EscrowEvidenceError } from './escrow-authority.types';
91
+ export type { HistoricalEscrowEvidence } from './escrow-authority.types';
92
+ export type { EscrowRecoveryExpectations } from '@learncard/types';
93
+ export {
94
+ verifyEscrowAuthorityState,
95
+ verifyEscrowEnrollmentCommit,
96
+ createEscrowEnrollmentIntent,
97
+ resolveEscrowEnrollmentAction,
98
+ violatesCarryContactRule,
99
+ } from './escrow-authority';
100
+ export type { EscrowEnrollmentActionInput } from './escrow-authority';
101
+ export type {
102
+ VerifiedEnclaveIdentity,
103
+ EscrowAuthorityRecord,
104
+ EscrowAuthorityState,
105
+ VerifiedEscrowMutation,
106
+ } from './escrow-authority.types';
107
+ export { verifyNitroAttestationDocument, NitroAttestationError } from './escrow-nitro-attestation';
108
+ export type { NitroAttestationReason } from './escrow-nitro-attestation';
80
109
  export type { EscrowAttestationPolicy, EscrowHoldStatus, EscrowRecoveryStart } from './types';
81
- export type { EscrowEnrollmentOptions, EscrowEnrollmentState, EscrowPinStatus } from './types';
110
+ export type {
111
+ EscrowEnrollmentOptions,
112
+ EscrowEnrollmentState,
113
+ EscrowBlobStaleReason,
114
+ EscrowPinStatus,
115
+ } from './types';
82
116
  export {
83
117
  EscrowPinMismatchError,
84
118
  EscrowPinLockedError,