@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
|
@@ -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 {
|
|
110
|
+
export type {
|
|
111
|
+
EscrowEnrollmentOptions,
|
|
112
|
+
EscrowEnrollmentState,
|
|
113
|
+
EscrowBlobStaleReason,
|
|
114
|
+
EscrowPinStatus,
|
|
115
|
+
} from './types';
|
|
82
116
|
export {
|
|
83
117
|
EscrowPinMismatchError,
|
|
84
118
|
EscrowPinLockedError,
|