node-tpm2 0.0.4-beta.3 → 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 +361 -88
- package/api.js +107 -0
- package/docs/api-reference.md +915 -0
- package/docs/roadmap.md +220 -0
- package/docs/windows-pcp.md +18 -0
- package/index.d.ts +47 -23
- package/native.cjs +57 -52
- package/native.d.ts +26 -0
- package/package.json +12 -10
package/README.md
CHANGED
|
@@ -1,164 +1,437 @@
|
|
|
1
1
|
# node-tpm2
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Native TPM 2.0 for Node.js. Prebuilt binaries — no `tpm2-tools`, no `tpm2-tss`, no Rust at install time.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
10
|
+
if (!(await Tpm.isAvailable())) throw new Error('No TPM');
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
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:
|
|
70
|
+
qualifyingData: challenge,
|
|
17
71
|
});
|
|
18
72
|
|
|
19
|
-
//
|
|
73
|
+
// → POST { akPublicDer, message, signature, pcrSelection } to your backend
|
|
20
74
|
```
|
|
21
75
|
|
|
22
|
-
|
|
76
|
+
### Handle style (grouped API)
|
|
23
77
|
|
|
24
|
-
|
|
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
|
-
|
|
81
|
+
await using tpm = await Tpm.open();
|
|
32
82
|
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
+
```javascript
|
|
122
|
+
import { Tpm } from 'node-tpm2';
|
|
123
|
+
import { readFileSync } from 'node:fs';
|
|
61
124
|
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
+
### Credential activation (enrollment proof-of-possession)
|
|
71
151
|
|
|
72
152
|
```javascript
|
|
73
|
-
|
|
153
|
+
import { Tpm } from 'node-tpm2';
|
|
74
154
|
|
|
75
|
-
|
|
76
|
-
await
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
await ak.quote({ pcrSelection: [7], qualifyingData: nonce });
|
|
81
|
-
await ak.export(); // persist akBlob
|
|
82
|
-
await ak.activateCredential({ credentialBlob, secret });
|
|
164
|
+
### Errors
|
|
83
165
|
|
|
84
|
-
|
|
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`).
|
|
229
|
+
|
|
230
|
+
### Availability
|
|
231
|
+
|
|
232
|
+
```javascript
|
|
233
|
+
await Tpm.isAvailable(); // boolean, never throws
|
|
234
|
+
await Tpm.info(); // { manufacturer, firmwareVersion, isVirtual, spec }
|
|
235
|
+
```
|
|
236
|
+
|
|
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);
|
|
85
269
|
```
|
|
86
270
|
|
|
87
|
-
|
|
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
|
-
|
|
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
|
|
105
|
-
//
|
|
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
|
-
|
|
340
|
+
**Wire format** (native → JS): `__tpm2__code|message|suggestion|tpmRc|hresult` — empty trailing fields mean undefined.
|
|
111
341
|
|
|
112
|
-
|
|
342
|
+
**Stability:** error **codes** are semver-stable after `latest`. New codes may be added in minors; renames require a major.
|
|
113
343
|
|
|
114
|
-
|
|
344
|
+
### Stable error codes
|
|
115
345
|
|
|
116
|
-
| |
|
|
117
|
-
|
|
118
|
-
|
|
|
119
|
-
|
|
|
120
|
-
|
|
|
121
|
-
|
|
|
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
|
-
|
|
361
|
+
### TPM response code → `TpmError.code`
|
|
124
362
|
|
|
125
|
-
|
|
363
|
+
When the TPM returns a non-zero response code, the library classifies it:
|
|
126
364
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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 |
|
|
131
372
|
|
|
132
|
-
|
|
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.
|
|
133
374
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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 |
|
|
138
392
|
|
|
139
|
-
|
|
393
|
+
**Transport** errors from `/dev/tpmrm0` or TBS that mention permission denied are promoted to `ACCESS_DENIED`; other I/O errors stay `TRANSPORT_ERROR`.
|
|
140
394
|
|
|
141
|
-
|
|
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
|
|
142
411
|
|
|
143
412
|
| Platform | Status | Attestation key |
|
|
144
413
|
|----------|--------|-----------------|
|
|
145
|
-
| Linux
|
|
146
|
-
| Windows
|
|
147
|
-
| macOS |
|
|
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
|
+
---
|
|
148
419
|
|
|
149
|
-
##
|
|
420
|
+
## Contributing
|
|
150
421
|
|
|
151
422
|
```bash
|
|
152
423
|
git clone https://github.com/stacks0x/tpm2.git && cd tpm2
|
|
153
424
|
npm install && npm run build
|
|
425
|
+
cargo test --lib
|
|
426
|
+
npm run verify:package
|
|
154
427
|
node examples/smoke-test.mjs runtime
|
|
155
428
|
```
|
|
156
429
|
|
|
157
|
-
|
|
430
|
+
Docs: [getting-started.md](./docs/getting-started.md) · [windows-pcp.md](./docs/windows-pcp.md) · [roadmap.md](./docs/roadmap.md)
|
|
158
431
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
432
|
+
Low-level Rust validation: `cargo run --no-default-features --features probe-bin --bin tbs-probe --` (repo only, not published to npm).
|
|
433
|
+
|
|
434
|
+
---
|
|
162
435
|
|
|
163
436
|
## License
|
|
164
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
|
};
|