node-tpm2 0.0.4-beta.2 → 0.0.4-beta.4

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 CHANGED
@@ -1,161 +1,437 @@
1
1
  # node-tpm2
2
2
 
3
- **TPM 2.0 attestation for Node.js** prebuilt native bindings, no `tpm2-tools`, no `tpm2-tss`, no Rust toolchain at install time.
3
+ Native TPM 2.0 for Node.js. Prebuilt binaries no `tpm2-tools`, no `tpm2-tss`, no Rust at install time.
4
4
 
5
- Use the TPM your OS already exposes: **TBS + Platform Crypto Provider** on Windows, **`/dev/tpmrm0`** on Linux. One small API for PCR reads, attestation key provisioning, quotes, and credential activation.
5
+ Talks to the TPM through OS-native paths: **TBS + Platform Crypto Provider** on Windows, **`/dev/tpmrm0`** on Linux. Returns buffers and typed records, not CLI text.
6
6
 
7
7
  ```javascript
8
8
  import { Tpm } from 'node-tpm2';
9
9
 
10
- const tpm = await Tpm.open();
10
+ if (!(await Tpm.isAvailable())) throw new Error('No TPM');
11
11
 
12
- const { akPublicDer, akBlob } = await tpm.attest.provisionAk();
13
- const quote = await tpm.attest.quote({
12
+ await using tpm = await Tpm.open();
13
+
14
+ const ak = await tpm.attest.provisionAk();
15
+ const { message, signature } = await ak.quote({
16
+ pcrSelection: [0, 1, 7],
17
+ qualifyingData: Buffer.from('challenge-nonce'),
18
+ });
19
+ ```
20
+
21
+ **Pre-release** (`0.0.x-beta`). [Roadmap](./docs/roadmap.md) for remaining namespaces.
22
+
23
+ ---
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install node-tpm2
29
+ ```
30
+
31
+ Node **20+**. One prebuilt `.node` per platform via optional dependencies.
32
+
33
+ ---
34
+
35
+ ## Examples
36
+
37
+ ### Check the TPM
38
+
39
+ ```javascript
40
+ import { Tpm } from 'node-tpm2';
41
+
42
+ if (!(await Tpm.isAvailable())) {
43
+ console.log('No TPM or no access');
44
+ process.exit(1);
45
+ }
46
+
47
+ const info = await Tpm.info();
48
+ console.log(info.manufacturer, info.firmwareVersion, info.isVirtual ? '(virtual)' : '');
49
+ ```
50
+
51
+ ### Device attestation (dev / same-user)
52
+
53
+ Provision a user-scoped attestation key, quote PCRs bound to a server challenge, send `message` + `signature` + `akPublicDer` to your verifier.
54
+
55
+ ```javascript
56
+ import { Tpm } from 'node-tpm2';
57
+ import { writeFileSync } from 'node:fs';
58
+
59
+ const challenge = Buffer.from('server-issued-nonce-or-session-id');
60
+
61
+ const { akPublicDer, akBlob } = await Tpm.provisionAk();
62
+ writeFileSync('ak.blob.json', JSON.stringify({
63
+ public: akBlob.public.toString('base64'),
64
+ private: akBlob.private.toString('base64'),
65
+ }));
66
+
67
+ const { message, signature } = await Tpm.quote({
14
68
  akBlob,
15
69
  pcrSelection: [0, 1, 7],
16
- qualifyingData: Buffer.from('your-challenge-nonce'),
70
+ qualifyingData: challenge,
17
71
  });
18
72
 
19
- // quote.message + quote.signature send to your verifier
73
+ // → POST { akPublicDer, message, signature, pcrSelection } to your backend
20
74
  ```
21
75
 
22
- ## Why node-tpm2
76
+ ### Handle style (grouped API)
23
77
 
24
- | | node-tpm2 | Shelling out to tpm2-tools |
25
- |---|-----------|----------------------------|
26
- | Install | `npm install` + prebuilt `.node` | OS packages, PATH, version drift |
27
- | API | Async JavaScript, structured errors | Parse CLI output |
28
- | Windows fleet | Machine-scoped PCP keys, cross-user quote | PCP/NCrypt scripting pain |
29
- | AK persistence | Wrapped blob (`akBlob`) — no persistent TPM handles in your app | Handle bookkeeping |
78
+ ```javascript
79
+ import { Tpm } from 'node-tpm2';
30
80
 
31
- **Use it when you need:**
81
+ await using tpm = await Tpm.open();
32
82
 
33
- - **Device attestation** prove boot/software state via PCR quotes bound to a challenge nonce
34
- - **Fleet enrollment** provision a machine-scoped attestation key at install time (Intune, SCCM, GPO); quote at runtime as the logged-in user
35
- - **Remote verification** — export AK public key (SPKI DER) and quote blobs to your backend; verify with standard TPM quote rules
36
- - **EK-backed onboarding** — read the EK certificate, activate credentials during enrollment
83
+ const pcrs = await tpm.pcr.read([0, 1, 7]);
84
+ const ekCert = await tpm.attest.ekCertificate(); // Buffer | null
37
85
 
38
- ## How it works
86
+ const ak = await tpm.attest.provisionAk();
87
+ const quote = await ak.quote({
88
+ pcrSelection: [0, 1, 7],
89
+ qualifyingData: Buffer.from('challenge'),
90
+ });
39
91
 
92
+ const saved = ak.export(); // persist { public, private } for next session
40
93
  ```
41
- ┌─────────────────────────────────────────────────────────┐
42
- │ Your Node app (Tpm.open / flat helpers) │
43
- └──────────────────────────┬──────────────────────────────┘
44
-
45
- ┌──────────────────────────▼──────────────────────────────┐
46
- │ node-tpm2 (napi-rs) command build, sessions, blobs │
47
- └──────────────┬─────────────────────────┬────────────────┘
48
- │ │
49
- Linux │ │ Windows
50
- ▼ ▼
51
- /dev/tpmrm0 TBS + NCrypt PCP
52
- ECDSA P-256 AK RSA-2048 persisted AK
53
- TPM2B wrapped blob PCP1 (user) / PCP2 (machine)
94
+
95
+ ### Windows fleet enrollment
96
+
97
+ **Threat model:** A machine AK proves **this enrolled device**, not which app or user quoted. The blob is a locator (`keyName`), not a secret — see [Threat model in windows-pcp.md](./docs/windows-pcp.md#threat-model-device-vs-application).
98
+
99
+ **Once** at install time (Admin or SYSTEM): create a machine-scoped key with a stable name and persist the blob.
100
+
101
+ ```javascript
102
+ import { Tpm } from 'node-tpm2';
103
+ import { writeFileSync } from 'node:fs';
104
+
105
+ // Run elevated or as SYSTEM — see docs/windows-pcp.md
106
+ const { akPublicDer, akBlob } = await Tpm.provisionAk({
107
+ keyName: 'my-app-device-ak',
108
+ scope: 'machine',
109
+ overwrite: true,
110
+ });
111
+
112
+ writeFileSync('C:\\ProgramData\\my-app\\ak.blob.json', JSON.stringify({
113
+ public: akBlob.public.toString('base64'),
114
+ private: akBlob.private.toString('base64'),
115
+ }));
116
+ // Register akPublicDer + creation data with your enrollment service
54
117
  ```
55
118
 
56
- - **No persistent TPM handles in your process.** You keep an `akBlob` (portable wrapped key material). Each operation loads transiently, signs, and flushes.
57
- - **Platform-native AK formats.** Linux uses ECDSA P-256 TPM2B blobs; Windows uses Microsoft PCP (`PCP1` user / `PCP2` machine). Verifiers should accept both.
58
- - **Structured errors.** Failures throw `TpmError` with stable `code`, optional `suggestion`, `tpmRc` (TPM return code), and `hresult` (Windows NCrypt).
119
+ **Every runtime session** (standard user): load the blob and quote no elevation.
59
120
 
60
- ## Install
121
+ ```javascript
122
+ import { Tpm } from 'node-tpm2';
123
+ import { readFileSync } from 'node:fs';
61
124
 
62
- ```bash
63
- npm install node-tpm2
125
+ const raw = JSON.parse(readFileSync('C:\\ProgramData\\my-app\\ak.blob.json', 'utf8'));
126
+ const akBlob = {
127
+ public: Buffer.from(raw.public, 'base64'),
128
+ private: Buffer.from(raw.private, 'base64'),
129
+ };
130
+
131
+ const quote = await Tpm.quote({
132
+ akBlob,
133
+ pcrSelection: [0, 1, 7],
134
+ qualifyingData: Buffer.from('runtime-challenge'),
135
+ });
64
136
  ```
65
137
 
66
- Node **20+**. npm installs a prebuilt native binary for your OS/arch via optional platform packages.
138
+ ### Read PCRs and TPM objects
139
+
140
+ ```javascript
141
+ import { Tpm } from 'node-tpm2';
142
+
143
+ await using tpm = await Tpm.open();
67
144
 
68
- ## API at a glance
145
+ const digests = await tpm.pcr.read([0, 1, 7]); // { 0: 'abc…', … }
146
+ const ek = await tpm.readPublic('0x81010001'); // endorsement key
147
+ const { publicKeyDer, name } = ek;
148
+ ```
69
149
 
70
- **Handle style** (grouped operations):
150
+ ### Credential activation (enrollment proof-of-possession)
71
151
 
72
152
  ```javascript
73
- const tpm = await Tpm.open();
153
+ import { Tpm } from 'node-tpm2';
74
154
 
75
- await tpm.info(); // manufacturer, firmware, virtual-TPM hint
76
- await tpm.pcr.read([0, 1, 7]); // SHA-256 PCR digests
77
- await tpm.attest.ekCertificate(); // EK cert from NV, or null
155
+ // credentialBlob + secret from your verifier's MakeCredential step
156
+ const recovered = await Tpm.activateCredential({
157
+ akBlob,
158
+ credentialBlob,
159
+ secret,
160
+ });
161
+ // recovered → proves AK is on the TPM that owns the EK
162
+ ```
78
163
 
79
- const ak = await tpm.attest.provisionAk(); // returns AkHandle
80
- await ak.quote({ pcrSelection: [7], qualifyingData: nonce });
81
- await ak.export(); // persist akBlob
82
- await ak.activateCredential({ credentialBlob, secret });
164
+ ### Errors
165
+
166
+ ```javascript
167
+ import { Tpm, TpmError } from 'node-tpm2';
168
+
169
+ try {
170
+ await Tpm.provisionAk({ scope: 'machine', keyName: 'fleet-ak' });
171
+ } catch (err) {
172
+ if (err instanceof TpmError && err.code === 'REQUIRES_ELEVATION') {
173
+ // Windows: run enrollment elevated or as SYSTEM, not at runtime
174
+ }
175
+ }
176
+ ```
177
+
178
+ More detail: [getting-started.md](./docs/getting-started.md) · [api-reference.md](./docs/api-reference.md) · [windows-pcp.md](./docs/windows-pcp.md) · [Error reference](#error-reference)
179
+
180
+ ---
181
+
182
+ ## Privilege matrix
183
+
184
+ **Legend:** ✓ standard user (with normal TPM access) · ✗ needs elevation · — not applicable · \* policy/firmware may block
185
+
186
+ | API | Linux standard user | Windows standard user | Windows Admin / SYSTEM |
187
+ |-----|:-------------------:|:---------------------:|:----------------------:|
188
+ | **Root** | | | |
189
+ | `Tpm.isAvailable()` | ✓ | ✓ | ✓ |
190
+ | `Tpm.open()` | ✓ | ✓ | ✓ |
191
+ | `tpm.info()` | ✓ | ✓ | ✓ |
192
+ | `tpm.readPublic(handle)` | ✓ | ✓ | ✓ |
193
+ | **random** | | | |
194
+ | `tpm.random.bytes(n)` | ✓ | ✓ | ✓ |
195
+ | **pcr** | | | |
196
+ | `tpm.pcr.read(...)` | ✓ | ✓ | ✓ |
197
+ | `tpm.pcr.extend(i, digest)` | ✓ * | ✓ * | ✓ |
198
+ | **nv** | | | |
199
+ | `tpm.nv.read(...)` | ✓ *planned* | ✓ *planned* | ✓ |
200
+ | `tpm.nv.write(...)` | ✓ *planned* | ✓ *planned* | ✓ |
201
+ | `tpm.attest.ekCertificate()` | ✓ | ✓ | ✓ |
202
+ | **keys** | | | |
203
+ | `tpm.keys.create(...)` | ✓ | ✓ | ✓ |
204
+ | `tpm.keys.load(blob)` | ✓ | ✓ | ✓ |
205
+ | `key.sign(digest)` | ✓ | ✓ | ✓ |
206
+ | `key.decrypt(cipher)` | — *planned* | — *planned* | — *planned* |
207
+ | **seal** | | | |
208
+ | `tpm.seal(...)` | ✓ *planned* | ✓ *planned* | ✓ |
209
+ | `tpm.unseal(blob)` | ✓ *planned* | ✓ *planned* | ✓ |
210
+ | **attest** | | | |
211
+ | `tpm.attest.provisionAk()` user | ✓ | ✓ | ✓ |
212
+ | `tpm.attest.provisionAk({ scope: 'machine' })` | — | ✗ | ✓ |
213
+ | `ak.quote(...)` / `Tpm.quote(...)` | ✓ | ✓ | ✓ |
214
+ | `ak.activateCredential(...)` | ✓ | ✗ | ✓ |
215
+
216
+ **Linux standard user** requires read/write on `/dev/tpmrm0` (commonly the `tss` group). That is a one-time deploy permission, not root for every call.
217
+
218
+ **Windows fleet pattern:** provision machine AK elevated or as SYSTEM once → persist `akBlob` → standard users quote forever after. See [docs/windows-pcp.md](./docs/windows-pcp.md).
219
+
220
+ **Planned rows** are design targets from the [roadmap](./docs/roadmap.md); unprivileged use matches the Phase 0 spike (`GetRandom`, `CreatePrimary` succeeded on Windows 11 without admin). Firmware or group policy can still deny specific PCR/NV operations — those surface as `TPM_RC` or `COMMAND_BLOCKED`, not silent failure.
221
+
222
+ ---
223
+
224
+ ## API reference (shipped)
225
+
226
+ Import: `import { Tpm, TpmError } from 'node-tpm2'`
227
+
228
+ All flat methods also exist on `Tpm.*` (e.g. `Tpm.pcrRead` ≡ `tpm.pcr.read`).
83
229
 
84
- await using tpm = await Tpm.open(); // Symbol.asyncDispose when done
230
+ ### Availability
231
+
232
+ ```javascript
233
+ await Tpm.isAvailable(); // boolean, never throws
234
+ await Tpm.info(); // { manufacturer, firmwareVersion, isVirtual, spec }
85
235
  ```
86
236
 
87
- **Flat style** (same operations, functional entry points):
237
+ ### Handle
238
+
239
+ ```javascript
240
+ await using tpm = await Tpm.open();
241
+ await tpm.readPublic('0x81000001'); // → { publicKeyDer, name }
242
+ ```
243
+
244
+ ### PCR
245
+
246
+ ```javascript
247
+ await tpm.pcr.read([0, 1, 7], 'sha256'); // → { 0: 'hex…', 1: 'hex…', … }
248
+ await tpm.pcr.extend(7, digest); // digest: 32-byte Buffer (SHA-256 bank)
249
+ await Tpm.pcrExtend(7, digest); // flat
250
+ ```
251
+
252
+ ### Random
253
+
254
+ ```javascript
255
+ await tpm.random.bytes(32); // Buffer from TPM2_GetRandom
256
+ await Tpm.randomBytes(32); // flat
257
+ ```
258
+
259
+ ### Keys (device-bound signing)
260
+
261
+ ```javascript
262
+ const key = await tpm.keys.create({ type: 'ecc', sign: true });
263
+ const digest = crypto.createHash('sha256').update('payload').digest();
264
+ const signature = await key.sign(digest);
265
+ const saved = key.export();
266
+
267
+ const reloaded = await tpm.keys.load(saved);
268
+ await reloaded.sign(digest);
269
+ ```
270
+
271
+ Flat: `Tpm.createKey()`, `Tpm.signKeyBlob({ keyBlob, digest })`. RSA `decrypt` is not yet implemented.
272
+
273
+ ### Attestation
274
+
275
+ ```javascript
276
+ const ak = await tpm.attest.provisionAk({
277
+ keyName: 'my-app-device-ak', // Windows: required for machine scope
278
+ scope: 'machine', // Windows: 'user' | 'machine'
279
+ overwrite: true,
280
+ });
281
+
282
+ const akBlob = ak.export(); // { public, private }
283
+ await ak.quote({ pcrSelection: [0, 1, 7], qualifyingData: Buffer.from('nonce') });
284
+ await tpm.attest.ekCertificate(); // Buffer | null
285
+ await ak.activateCredential({ credentialBlob, secret });
286
+ ```
287
+
288
+ ### Flat equivalents
88
289
 
89
290
  ```javascript
90
- await Tpm.isAvailable();
91
- await Tpm.provisionAk({ keyName: 'my-app-ak' });
92
- await Tpm.quote({ akBlob, pcrSelection: [0], qualifyingData: nonce });
93
291
  await Tpm.pcrRead([0, 1, 7]);
292
+ await Tpm.readPublic('0x81010001');
94
293
  await Tpm.readEkCertificate();
294
+ await Tpm.provisionAk({ scope: 'user' });
295
+ await Tpm.quote({ akBlob, pcrSelection: [7], qualifyingData: nonce });
95
296
  await Tpm.activateCredential({ akBlob, credentialBlob, secret });
96
297
  ```
97
298
 
98
- Errors:
299
+ ### Types
300
+
301
+ ```typescript
302
+ type AkBlob = { public: Buffer; private: Buffer };
303
+
304
+ type ProvisionAkOptions = {
305
+ keyName?: string;
306
+ scope?: 'user' | 'machine';
307
+ overwrite?: boolean;
308
+ };
309
+
310
+ type QuoteOptions = {
311
+ akBlob: AkBlob;
312
+ pcrSelection: number[];
313
+ qualifyingData: Buffer;
314
+ bank?: 'sha256';
315
+ };
316
+ ```
317
+
318
+ ---
319
+
320
+ ## Error reference
321
+
322
+ Failures throw `TpmError` (subclass of `Error`). Inspect **`code`** for programmatic handling; use **`message`** for logs; **`tpmRc`** / **`hresult`** carry raw platform codes when present.
99
323
 
100
324
  ```javascript
325
+ import { Tpm, TpmError } from 'node-tpm2';
326
+
101
327
  try {
102
328
  await Tpm.provisionAk({ scope: 'machine', keyName: 'fleet-ak' });
103
329
  } catch (err) {
104
- if (err.code === 'REQUIRES_ELEVATION') {
105
- // Windows: machine keys need Admin/SYSTEM at provision time only
330
+ if (err instanceof TpmError) {
331
+ err.code; // stable string branch on this
332
+ err.message; // human detail (includes context + hex codes)
333
+ err.suggestion; // optional remediation
334
+ err.tpmRc; // TPM 2.0 response code (number), when applicable
335
+ err.hresult; // Windows NCrypt / Win32 HRESULT (number), when applicable
106
336
  }
107
337
  }
108
338
  ```
109
339
 
110
- Full reference: [docs/getting-started.md](./docs/getting-started.md) · Windows fleet: [docs/windows-pcp.md](./docs/windows-pcp.md)
340
+ **Wire format** (native → JS): `__tpm2__code|message|suggestion|tpmRc|hresult` empty trailing fields mean undefined.
111
341
 
112
- ## Privileges (honest summary)
342
+ **Stability:** error **codes** are semver-stable after `latest`. New codes may be added in minors; renames require a major.
113
343
 
114
- There is **no separate TPM daemon to install**, but the OS still controls access:
344
+ ### Stable error codes
115
345
 
116
- | | Linux | Windows |
117
- |---|-------|---------|
118
- | **Typical runtime** (quote, PCR read) | User in `tss` group (or equivalent access to `/dev/tpmrm0`) | Standard user no elevation |
119
- | **User-scoped AK** (`provisionAk()`) | Same as runtime | Standard user |
120
- | **Machine-scoped AK** (`scope: 'machine'`) | N/A | **Admin or SYSTEM at enrollment** then standard users quote with the saved blob |
121
- | **Credential activation** | TPM policy dependent | Elevated / SYSTEM |
346
+ | Code | When | `tpmRc` | `hresult` | Typical `suggestion` |
347
+ |------|------|:-------:|:---------:|----------------------|
348
+ | `TPM_UNAVAILABLE` | No TPM, no native binary, macOS, or backend not built | | | Install platform package / check TPM |
349
+ | `ACCESS_DENIED` | OS denied device or key access | | sometimes | Linux: `tss` group; container: pass device |
350
+ | `REQUIRES_ELEVATION` | Windows operation needs Admin/SYSTEM || | Re-run enrollment elevated or as SYSTEM |
351
+ | `COMMAND_BLOCKED` | Windows TBS driver blocked the command ordinal | | | Use NCrypt PCP path (e.g. activation) |
352
+ | `NOT_SUPPORTED` | Feature or PCP capability missing on this platform | — | sometimes | — |
353
+ | `INVALID_ARGUMENT` | Bad JS/Rust option (e.g. empty machine `keyName`) | — | sometimes | Fix caller input |
354
+ | `KEY_NOT_FOUND` | NCrypt key / blob locator not found | — | ✓ | Check persisted blob / key name |
355
+ | `ALREADY_EXISTS` | NCrypt key name already exists | — | ✓ | Use `overwrite: true` |
356
+ | `MARSHALLING_ERROR` | Codec bug, malformed TPM command, or unclassified NCrypt failure | sometimes | sometimes | Report bug or check firmware |
357
+ | `TRANSPORT_ERROR` | TBS / `/dev/tpmrm0` I/O failure | — | — | Retry; check driver / device node |
358
+ | `AUTH_FAILED` | TPM auth-class response (policy / password / hierarchy) | ✓ | — | Check object auth or policy |
359
+ | `TPM_RC` | Other TPM non-success response | ✓ | — | See `tpmRc` nibble / TPM spec |
122
360
 
123
- **Fleet pattern:** installer runs once elevated (or as SYSTEM) saves `akBlob` → app quotes unprivileged forever after. See [docs/windows-pcp.md](./docs/windows-pcp.md).
361
+ ### TPM response code → `TpmError.code`
124
362
 
125
- ## Validate your install
363
+ When the TPM returns a non-zero response code, the library classifies it:
126
364
 
127
- ```bash
128
- node node_modules/node-tpm2/examples/smoke-test.mjs runtime
129
- ```
365
+ | TPM RC class | Condition | Maps to | Example `tpmRc` |
366
+ |--------------|-----------|---------|-----------------|
367
+ | Success | `rc === 0` | (no error) | `0` |
368
+ | Auth | `(rc & 0x0300) === 0x0300` | `AUTH_FAILED` | `0x38E` (`TPM_RC_AUTH_FAIL`) |
369
+ | Format | `(rc & 0xFF00) === 0x0100` or FMT1 bit set | `MARSHALLING_ERROR` | `0x125` (`TPM_RC_ASYMMETRIC`) |
370
+ | Windows TBS blocked | `rc === 0x80280400` | `COMMAND_BLOCKED` | `0x80280400` |
371
+ | Other | everything else | `TPM_RC` | vendor-specific |
130
372
 
131
- Windows fleet smoke (elevated provision, then standard-user quote):
373
+ Auth-class and format-class detection follows TPM 2.0 response-code layout (see `src/tbs/rc.rs`). **`tpmRc` on the error is the full 32-bit value** from the TPM response header — use it for logs and TPM spec lookup.
132
374
 
133
- ```bash
134
- node node_modules/node-tpm2/examples/smoke-test.mjs provision-machine --key-name my-app-device-ak --out ak.blob.json
135
- node node_modules/node-tpm2/examples/smoke-test.mjs quote --in ak.blob.json
136
- ```
375
+ ### Windows NCrypt HRESULT → `TpmError.code`
376
+
377
+ PCP / NCrypt failures on Windows map through `classify_ncrypt` (`src/tbs/ncrypt.rs`):
378
+
379
+ | HRESULT | Name | Typical code | Notes |
380
+ |---------|------|--------------|-------|
381
+ | `0x80090011` | `NTE_NOT_FOUND` | `KEY_NOT_FOUND` | Missing persisted key |
382
+ | `0x80090016` | `NTE_BAD_KEYSET` | `KEY_NOT_FOUND` | Key set not found |
383
+ | `0x8009000B` | `NTE_EXISTS` | `ALREADY_EXISTS` | Key name collision |
384
+ | `0x80090027` | `NTE_INVALID_PARAMETER` | `INVALID_ARGUMENT` | Bad NCrypt parameter |
385
+ | `0x80090030` | `NTE_DEVICE_NOT_READY` | `REQUIRES_ELEVATION` | Often privilege / readiness |
386
+ | `0x80090010` | `NTE_PERM` | `REQUIRES_ELEVATION` | Permission |
387
+ | `0x80090029` | `NTE_BAD_FLAGS` | `REQUIRES_ELEVATION` | Bad flags |
388
+ | `0x8009000F` | `NTE_INTERNAL_ERROR` | `REQUIRES_ELEVATION` | Machine provision from standard user (observed) |
389
+ | `0x80280084` | PCP activation / `TPM_RC_VALUE` | `REQUIRES_ELEVATION` | Standard user activation; elevated → `MARSHALLING_ERROR` |
390
+ | `0x5` / `0x80070005` | Access denied | `REQUIRES_ELEVATION` or `ACCESS_DENIED` | Machine provision → elevation |
391
+ | (other) | — | `MARSHALLING_ERROR` | Unmapped NCrypt failure |
392
+
393
+ **Transport** errors from `/dev/tpmrm0` or TBS that mention permission denied are promoted to `ACCESS_DENIED`; other I/O errors stay `TRANSPORT_ERROR`.
137
394
 
138
- ## Platform support
395
+ ---
396
+
397
+ ## API reference (planned)
398
+
399
+ Subsystem namespaces not yet on `TpmHandle`. See [docs/roadmap.md](./docs/roadmap.md) for phases and acceptance criteria.
400
+
401
+ | Namespace | Methods |
402
+ |-----------|---------|
403
+ | `tpm.random` | `bytes(n)` ✅ |
404
+ | `tpm.keys` | `create`, `load`, `KeyHandle.sign` ✅ · `decrypt` planned |
405
+ | `tpm.pcr` | `extend(index, digest)` |
406
+ | `tpm.seal` | `seal`, `unseal` |
407
+
408
+ ---
409
+
410
+ ## Platforms
139
411
 
140
412
  | Platform | Status | Attestation key |
141
413
  |----------|--------|-----------------|
142
- | Linux (glibc/musl, x64/arm64) | Supported | ECDSA P-256 TPM2B |
143
- | Windows (x64/arm64) | Supported | RSA-2048 PCP |
144
- | macOS | Not supported (`isAvailable()` → false) | — |
414
+ | Linux x64/arm64 gnu/musl | Supported | ECDSA P-256 TPM2B |
415
+ | Windows x64/arm64 | Supported | RSA-2048 PCP |
416
+ | macOS | Unavailable | `isAvailable()` → `false` |
417
+
418
+ ---
145
419
 
146
- ## Development
420
+ ## Contributing
147
421
 
148
422
  ```bash
149
423
  git clone https://github.com/stacks0x/tpm2.git && cd tpm2
150
424
  npm install && npm run build
425
+ cargo test --lib
426
+ npm run verify:package
151
427
  node examples/smoke-test.mjs runtime
152
428
  ```
153
429
 
154
- Rust probe for low-level validation (repo only, **not** published to npm):
430
+ Docs: [getting-started.md](./docs/getting-started.md) · [windows-pcp.md](./docs/windows-pcp.md) · [roadmap.md](./docs/roadmap.md)
155
431
 
156
- ```powershell
157
- cargo run --no-default-features --features probe-bin --bin tbs-probe -- all
158
- ```
432
+ Low-level Rust validation: `cargo run --no-default-features --features probe-bin --bin tbs-probe --` (repo only, not published to npm).
433
+
434
+ ---
159
435
 
160
436
  ## License
161
437
 
package/api.js CHANGED
@@ -66,6 +66,38 @@ export class TpmError extends Error {
66
66
  }
67
67
  }
68
68
 
69
+ function notSupported(feature) {
70
+ return async () => {
71
+ throw new TpmError('NOT_SUPPORTED', `${feature} is not implemented yet.`);
72
+ };
73
+ }
74
+
75
+ function createKeyHandle(publicKeyDer, keyBlob) {
76
+ return {
77
+ export() {
78
+ return {
79
+ public: Buffer.from(keyBlob.public),
80
+ private: Buffer.from(keyBlob.private),
81
+ };
82
+ },
83
+
84
+ get publicKeyDer() {
85
+ return Buffer.from(publicKeyDer);
86
+ },
87
+
88
+ sign: wrapNative(async (digest) => {
89
+ requireNative('signKeyBlob');
90
+ const sig = await native.signKeyBlob({
91
+ keyBlob,
92
+ digest,
93
+ });
94
+ return Buffer.from(sig);
95
+ }),
96
+
97
+ decrypt: notSupported('key.decrypt'),
98
+ };
99
+ }
100
+
69
101
  function createAkHandle(akPublicDer, akBlob) {
70
102
  return {
71
103
  /** Wrapped TPM2B_PUBLIC + TPM2B_PRIVATE for persistence (no persistent TPM handle). */
@@ -114,6 +146,49 @@ function createTpmHandle() {
114
146
  requireNative('pcrRead');
115
147
  return native.pcrRead(selection, bank);
116
148
  }),
149
+
150
+ /** Extend a PCR digest in the SHA-256 bank. */
151
+ extend: wrapNative(async (index, digest) => {
152
+ requireNative('pcrExtend');
153
+ await native.pcrExtend(index, digest);
154
+ }),
155
+ },
156
+
157
+ random: {
158
+ /** Read `count` bytes from the TPM RNG (GetRandom). */
159
+ bytes: wrapNative(async (count) => {
160
+ requireNative('randomBytes');
161
+ const buf = await native.randomBytes(count);
162
+ return Buffer.from(buf);
163
+ }),
164
+ },
165
+
166
+ nv: {
167
+ read: notSupported('tpm.nv.read'),
168
+ write: notSupported('tpm.nv.write'),
169
+ },
170
+
171
+ keys: {
172
+ create: wrapNative(async (opts) => {
173
+ requireNative('createKey');
174
+ const result = await native.createKey({
175
+ keyType: opts?.type,
176
+ sign: opts?.sign,
177
+ decrypt: opts?.decrypt,
178
+ });
179
+ return createKeyHandle(result.publicKeyDer, result.keyBlob);
180
+ }),
181
+
182
+ load: wrapNative(async (blob) => {
183
+ requireNative('keyBlobPublicDer');
184
+ const publicKeyDer = await native.keyBlobPublicDer(blob);
185
+ return createKeyHandle(publicKeyDer, blob);
186
+ }),
187
+ },
188
+
189
+ seal: {
190
+ seal: notSupported('tpm.seal'),
191
+ unseal: notSupported('tpm.unseal'),
117
192
  },
118
193
 
119
194
  attest: {
@@ -202,12 +277,25 @@ export const Tpm = {
202
277
  return this.getFixedProperties();
203
278
  },
204
279
 
280
+ /** Flat: TPM RNG bytes (GetRandom). Prefer `tpm.random.bytes` on an open handle. */
281
+ randomBytes: wrapNative(async (count) => {
282
+ requireNative('randomBytes');
283
+ const buf = await native.randomBytes(count);
284
+ return Buffer.from(buf);
285
+ }),
286
+
205
287
  /** Flat native binding: PCR read. Prefer `tpm.pcr.read` on an open handle. */
206
288
  pcrRead: wrapNative(async (selection, bank) => {
207
289
  requireNative('pcrRead');
208
290
  return native.pcrRead(selection, bank);
209
291
  }),
210
292
 
293
+ /** Flat native binding: PCR extend. Prefer `tpm.pcr.extend` on an open handle. */
294
+ pcrExtend: wrapNative(async (index, digest) => {
295
+ requireNative('pcrExtend');
296
+ await native.pcrExtend(index, digest);
297
+ }),
298
+
211
299
  /** Flat native binding: ReadPublic. */
212
300
  readPublic: wrapNative(async (handle) => {
213
301
  requireNative('readPublic');
@@ -245,4 +333,23 @@ export const Tpm = {
245
333
  requireNative('activateCredential');
246
334
  return native.activateCredential(opts);
247
335
  }),
336
+
337
+ createKey: wrapNative(async (opts) => {
338
+ requireNative('createKey');
339
+ const result = await native.createKey({
340
+ keyType: opts?.type,
341
+ sign: opts?.sign,
342
+ decrypt: opts?.decrypt,
343
+ });
344
+ return {
345
+ publicKeyDer: result.publicKeyDer,
346
+ keyBlob: result.keyBlob,
347
+ };
348
+ }),
349
+
350
+ signKeyBlob: wrapNative(async (opts) => {
351
+ requireNative('signKeyBlob');
352
+ const sig = await native.signKeyBlob(opts);
353
+ return Buffer.from(sig);
354
+ }),
248
355
  };