node-tpm2

Native TPM 2.0 for Node.js. Prebuilt binaries — no tpm2-tools, no tpm2-tss, no Rust at install time.
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.
import { Tpm } from 'node-tpm2';
if (!(await Tpm.isAvailable())) throw new Error('No TPM');
await using tpm = await Tpm.open();
const ak = await tpm.attest.provisionAk();
const { message, signature } = await ak.quote({
pcrSelection: [0, 1, 7],
qualifyingData: Buffer.from('challenge-nonce'),
});
Stable (0.0.8). Full public API implemented and validated on real Windows 11 + Intel TPM and Linux (STM fTPM). API reference · Roadmap.
Install
npm install node-tpm2
Node 20+. One prebuilt .node per platform via optional dependencies.
Examples
Check the TPM
import { Tpm } from 'node-tpm2';
if (!(await Tpm.isAvailable())) {
console.log('No TPM or no access');
process.exit(1);
}
const info = await Tpm.info();
console.log(info.manufacturer, info.firmwareVersion, info.isVirtual ? '(virtual)' : '');
Device attestation (dev / same-user)
Provision a user-scoped attestation key, quote PCRs bound to a server challenge, send message + signature + akPublicDer to your verifier.
import { Tpm } from 'node-tpm2';
import { writeFileSync } from 'node:fs';
const challenge = Buffer.from('server-issued-nonce-or-session-id');
const { akPublicDer, akBlob } = await Tpm.provisionAk();
writeFileSync('ak.blob.json', JSON.stringify({
public: akBlob.public.toString('base64'),
private: akBlob.private.toString('base64'),
}));
const { message, signature } = await Tpm.quote({
akBlob,
pcrSelection: [0, 1, 7],
qualifyingData: challenge,
});
Handle style (grouped API)
import { Tpm } from 'node-tpm2';
await using tpm = await Tpm.open();
const pcrs = await tpm.pcr.read([0, 1, 7]);
const ekCert = await tpm.attest.ekCertificate();
const ak = await tpm.attest.provisionAk();
const quote = await ak.quote({
pcrSelection: [0, 1, 7],
qualifyingData: Buffer.from('challenge'),
});
const saved = ak.export();
Windows fleet enrollment
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.
Once at install time (Admin or SYSTEM): create a machine-scoped key with a stable name and persist the blob.
import { Tpm } from 'node-tpm2';
import { writeFileSync } from 'node:fs';
const { akPublicDer, akBlob } = await Tpm.provisionAk({
keyName: 'my-app-device-ak',
scope: 'machine',
overwrite: true,
});
writeFileSync('C:\\ProgramData\\my-app\\ak.blob.json', JSON.stringify({
public: akBlob.public.toString('base64'),
private: akBlob.private.toString('base64'),
}));
Every runtime session (standard user): load the blob and quote — no elevation.
import { Tpm } from 'node-tpm2';
import { readFileSync } from 'node:fs';
const raw = JSON.parse(readFileSync('C:\\ProgramData\\my-app\\ak.blob.json', 'utf8'));
const akBlob = {
public: Buffer.from(raw.public, 'base64'),
private: Buffer.from(raw.private, 'base64'),
};
const quote = await Tpm.quote({
akBlob,
pcrSelection: [0, 1, 7],
qualifyingData: Buffer.from('runtime-challenge'),
});
Read PCRs and TPM objects
import { Tpm } from 'node-tpm2';
await using tpm = await Tpm.open();
const digests = await tpm.pcr.read([0, 1, 7]);
const ek = await tpm.readPublic('0x81010001');
const { publicKeyDer, name } = ek;
Credential activation (enrollment proof-of-possession)
import { Tpm } from 'node-tpm2';
const recovered = await Tpm.activateCredential({
akBlob,
credentialBlob,
secret,
});
Errors
import { Tpm, TpmError } from 'node-tpm2';
try {
await Tpm.provisionAk({ scope: 'machine', keyName: 'fleet-ak' });
} catch (err) {
if (err instanceof TpmError && err.code === 'REQUIRES_ELEVATION') {
}
}
More detail: getting-started.md · api-reference.md · windows-pcp.md · Error reference
Privilege matrix
Legend: ✓ standard user (with normal TPM access) · ✗ needs elevation · — not applicable · * policy/firmware may block
| Root | | | |
Tpm.isAvailable() | ✓ | ✓ | ✓ |
Tpm.open() | ✓ | ✓ | ✓ |
tpm.info() | ✓ | ✓ | ✓ |
tpm.readPublic(handle) | ✓ | ✓ | ✓ |
| random | | | |
tpm.random.bytes(n) | ✓ | ✓ | ✓ |
| pcr | | | |
tpm.pcr.read(...) | ✓ | ✓ | ✓ |
tpm.pcr.extend(i, digest) | ✓ † | ✗ → REQUIRES_ELEVATION | ✓ † |
| nv | | | |
tpm.nv.read(...) | ✓ ‡ | ✓ ‡ | ✓ |
tpm.nv.write(...) | ✓ ‡ | ✓ ‡ | ✓ |
tpm.nv.readPublic(...) | ✓ ‡ | ✓ ‡ ¶ | ✓ |
tpm.nv.define(...) | ✓ § | ✗ → REQUIRES_ELEVATION | ✓ § |
tpm.nv.undefine(...) | ✓ § | ✗ → REQUIRES_ELEVATION | ✓ § |
tpm.attest.ekCertificate() | ✓ | ✓ | ✓ |
| keys | | | |
tpm.keys.create(...) | ✓ | ✓ | ✓ |
tpm.keys.load(blob) | ✓ | ✓ | ✓ |
key.sign(digest) | ✓ | ✓ | ✓ |
key.decrypt(cipher) | ✓ | ✓ | ✓ |
| seal | | | |
tpm.seal.seal(...) | ✓ | ✓ | ✓ |
tpm.unseal(blob) | ✓ | ✓ | ✓ |
| attest | | | |
tpm.attest.akPublicDer(blob) / Tpm.readAkPublic(blob) | ✓ †† | ✓ †† | ✓ †† |
tpm.attest.provisionAk() user | ✓ | ✓ | ✓ |
tpm.attest.provisionAk({ scope: 'machine' }) | — | ✗ | ✓ |
ak.quote(...) / Tpm.quote(...) | ✓ | ✓ | ✓ |
ak.activateCredential(...) | ✓ | ✗ | ✓ |
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.
Windows fleet pattern: provision machine AK elevated or as SYSTEM once → persist akBlob → standard users quote forever after. See docs/windows-pcp.md.
†† readAkPublic / akPublicDer: Read-only SPKI extraction from a saved blob — no TPM quote, no re-provision. Linux: wire decode only. Windows PCP: NCrypt public-key export for the persisted key locator (standard user OK when the key exists).
Hardware validation (0.0.5): Windows 11 Intel TPM — attestation suite (user + machine AK, cross-user quote, credential activation elevated), random, keys (sign + RSA decrypt), pcr.read / pcr.extend (elevated), full nv cycle (define / write / read / undefine elevated; read/write standard user on existing indices). Linux: CI + swtpm. Firmware or group policy can still deny specific PCR/NV operations — those surface as TPM_RC or REQUIRES_ELEVATION, not silent failure.
‡ nv.read/write: Success depends on index attributes and auth. EK cert indices (0x01c00002, 0x01c0000A) are read-only.
§ nv.define/undefine: Owner NV range only (0x01800000–0x01BFFFFF). Requires owner authorization (often empty password). Consumes NV space until undefined — use only on test machines or with a chosen index. Windows standard user → REQUIRES_ELEVATION (same TBS block as pcr.extend); run Admin PowerShell.
Note: On Windows, raw TBS may reject nv.readPublic for owner-range indices (MARSHALLING_ERROR / TPM_RC ~0xA6) even when define/read/write succeed. Factory indices (0x01c00002 EK cert) work. After nv.define, use the known size for read/write; examples/nv-smoke.mjs handles this.
¶ nv.readPublic: Works for factory indices (EK cert). Owner-range indices often fail on Windows TBS — use size from nv.define or nv.read bounds instead.
† pcr.extend: Linux standard user (prefer indices 16–23 for experiments; avoid 0–7 boot/Secure Boot PCRs). Windows standard user → REQUIRES_ELEVATION (TPM_E_COMMAND_BLOCKED from TBS). Windows Administrator can extend on real hardware (validated). Standard-user failure is not COMMAND_BLOCKED — re-run elevated.
API reference (shipped)
Import: import { Tpm, TpmError } from 'node-tpm2'
All flat methods also exist on Tpm.* (e.g. Tpm.pcrRead ≡ tpm.pcr.read).
Availability
await Tpm.isAvailable();
await Tpm.info();
Handle
await using tpm = await Tpm.open();
await tpm.readPublic('0x81000001');
PCR
await tpm.pcr.read([0, 1, 7], 'sha256');
await tpm.pcr.extend(7, digest);
await Tpm.pcrExtend(7, digest);
Random
await tpm.random.bytes(32);
await Tpm.randomBytes(32);
Keys (device-bound signing)
const key = await tpm.keys.create({ type: 'ecc', sign: true });
const digest = crypto.createHash('sha256').update('payload').digest();
const signature = await key.sign(digest);
const saved = key.export();
const reloaded = await tpm.keys.load(saved);
await reloaded.sign(digest);
const rsaKey = await tpm.keys.create({ type: 'rsa', sign: true, decrypt: true });
const plain = await rsaKey.decrypt(ciphertext);
Flat: Tpm.createKey(), Tpm.signKeyBlob({ keyBlob, digest }), Tpm.decryptKeyBlob({ keyBlob, cipher }).
NV
await tpm.nv.read('0x01c00002');
await tpm.nv.readPublic('0x01800042');
await tpm.nv.define({ handle: '0x01800042', size: 64 });
await tpm.nv.write('0x01800042', data, 0);
await tpm.nv.undefine('0x01800042');
Flat: Tpm.nvRead, Tpm.nvWrite, Tpm.nvReadPublic, Tpm.nvDefine, Tpm.nvUndefine. See examples/nv-smoke.mjs.
Seal
const sealed = await tpm.seal.seal({ data: secret, pcrSelection: [23] });
const plain = await tpm.seal.unseal(sealed);
Flat: Tpm.seal, Tpm.unseal.
Attestation
const ak = await tpm.attest.provisionAk({
keyName: 'my-app-device-ak',
scope: 'machine',
overwrite: true,
});
const akBlob = ak.export();
await ak.quote({ pcrSelection: [0, 1, 7], qualifyingData: Buffer.from('nonce') });
await tpm.attest.ekCertificate();
await ak.activateCredential({ credentialBlob, secret });
Flat equivalents
await Tpm.pcrRead([0, 1, 7]);
await Tpm.readPublic('0x81010001');
await Tpm.readEkCertificate();
await Tpm.readAkPublic(akBlob);
await Tpm.provisionAk({ scope: 'user' });
await Tpm.quote({ akBlob, pcrSelection: [7], qualifyingData: nonce });
await Tpm.activateCredential({ akBlob, credentialBlob, secret });
Types
type AkBlob = { public: Buffer; private: Buffer };
type ProvisionAkOptions = {
keyName?: string;
scope?: 'user' | 'machine';
overwrite?: boolean;
};
type QuoteOptions = {
akBlob: AkBlob;
pcrSelection: number[];
qualifyingData: Buffer;
bank?: 'sha256';
};
Error reference
Failures throw TpmError (subclass of Error). Inspect code for programmatic handling; use message for logs; tpmRc / hresult carry raw platform codes when present.
import { Tpm, TpmError } from 'node-tpm2';
try {
await Tpm.provisionAk({ scope: 'machine', keyName: 'fleet-ak' });
} catch (err) {
if (err instanceof TpmError) {
err.code;
err.message;
err.suggestion;
err.tpmRc;
err.hresult;
}
}
Wire format (native → JS): __tpm2__code|message|suggestion|tpmRc|hresult — empty trailing fields mean undefined.
Stability: error codes are semver-stable after latest. New codes may be added in minors; renames require a major.
Stable error codes
TPM_UNAVAILABLE | No TPM, no native binary, macOS, or backend not built | — | — | Install platform package / check TPM |
ACCESS_DENIED | OS denied device or key access | — | sometimes | Linux: tss group; container: pass device |
REQUIRES_ELEVATION | Windows operation needs Admin/SYSTEM | — | ✓ | Re-run elevated; pcr.extend and nv.define/nv.undefine from standard user |
COMMAND_BLOCKED | Windows TBS blocked raw ordinal (e.g. ActivateCredential) | ✓ | — | Use NCrypt PCP — elevation does not help |
NOT_SUPPORTED | Feature or PCP capability missing on this platform | — | sometimes | — |
INVALID_ARGUMENT | Bad JS/Rust option (e.g. empty machine keyName) | — | sometimes | Fix caller input |
KEY_NOT_FOUND | NCrypt key / blob locator not found | — | ✓ | Check persisted blob / key name |
ALREADY_EXISTS | NCrypt key name already exists | — | ✓ | Use overwrite: true |
MARSHALLING_ERROR | Codec bug, malformed TPM command, or unclassified NCrypt failure | sometimes | sometimes | Report bug or check firmware |
TRANSPORT_ERROR | TBS / /dev/tpmrm0 I/O failure | — | — | Retry; check driver / device node |
AUTH_FAILED | TPM auth-class response (policy / password / hierarchy) | ✓ | — | Check object auth or policy |
TPM_RC | Other TPM non-success response | ✓ | — | See tpmRc nibble / TPM spec |
TPM response code → TpmError.code
When the TPM returns a non-zero response code, the library classifies it:
| Success | rc === 0 | (no error) | 0 |
| Auth | (rc & 0x0300) === 0x0300 | AUTH_FAILED | 0x38E (TPM_RC_AUTH_FAIL) |
| Format | (rc & 0xFF00) === 0x0100 or FMT1 bit set | MARSHALLING_ERROR | 0x125 (TPM_RC_ASYMMETRIC) |
| Windows TBS blocked | rc === 0x80280400 (most ordinals) | COMMAND_BLOCKED | 0x80280400 |
| Windows TBS blocked | rc === 0x80280400 (PCR_Extend, NV_DefineSpace, NV_UndefineSpace) | REQUIRES_ELEVATION | 0x80280400 |
| Other | everything else | TPM_RC | vendor-specific |
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.
Windows NCrypt HRESULT → TpmError.code
PCP / NCrypt failures on Windows map through classify_ncrypt (src/tbs/ncrypt.rs):
0x80090011 | NTE_NOT_FOUND | KEY_NOT_FOUND | Missing persisted key |
0x80090016 | NTE_BAD_KEYSET | KEY_NOT_FOUND | Key set not found |
0x8009000B | NTE_EXISTS | ALREADY_EXISTS | Key name collision |
0x80090027 | NTE_INVALID_PARAMETER | INVALID_ARGUMENT | Bad NCrypt parameter |
0x80090030 | NTE_DEVICE_NOT_READY | REQUIRES_ELEVATION | Often privilege / readiness |
0x80090010 | NTE_PERM | REQUIRES_ELEVATION | Permission |
0x80090029 | NTE_BAD_FLAGS | REQUIRES_ELEVATION | Bad flags |
0x8009000F | NTE_INTERNAL_ERROR | REQUIRES_ELEVATION | Machine provision from standard user (observed) |
0x80280084 | PCP activation / TPM_RC_VALUE | REQUIRES_ELEVATION | Standard user activation; elevated → MARSHALLING_ERROR |
0x5 / 0x80070005 | Access denied | REQUIRES_ELEVATION or ACCESS_DENIED | Machine provision → elevation |
| (other) | — | MARSHALLING_ERROR | Unmapped NCrypt failure |
Transport errors from /dev/tpmrm0 or TBS that mention permission denied are promoted to ACCESS_DENIED; other I/O errors stay TRANSPORT_ERROR.
API surface (complete)
Import: import { Tpm, TpmError } from 'node-tpm2'. Flat Tpm.* wrappers exist for every operation below.
| Root | Tpm.isAvailable(), Tpm.open(), tpm.info(), tpm.readPublic() | ✅ |
tpm.random | bytes(n) | ✅ |
tpm.pcr | read, extend | ✅ |
tpm.nv | read, write, readPublic, define, undefine | ✅ |
tpm.keys | create, load, KeyHandle.sign, KeyHandle.decrypt, KeyHandle.export | ✅ |
tpm.seal | seal, unseal | ✅ |
tpm.attest | provisionAk, quote, ekCertificate, AkHandle.activateCredential, AkHandle.export, AkHandle.publicKeyDer | ✅ |
Full signatures: docs/api-reference.md.
Platforms
| Linux x64/arm64 gnu/musl | Supported | ECDSA P-256 TPM2B |
| Windows x64/arm64 | Supported | RSA-2048 PCP |
| macOS | Unavailable | isAvailable() → false |
Supply chain transparency
This package is a native TPM binding (prebuilt .node + napi-rs loader). Socket.dev scores it highly on quality, license, and vulnerability, with a lower Supply Chain Security score (~71) that reflects structural native-module patterns, not a known defect.
Typical flags: dynamic require of platform binaries, filesystem reads for libc detection, env vars (NAPI_RS_*), and a hardcoded ldd --version shell fallback in the generated loader (Linux only, last resort). Each is documented in SECURITY.md.
We publish the Socket score and the full alert-by-alert accounting voluntarily — see SECURITY.md for details and how to report security issues.
Contributing
git clone https://github.com/stacks0x/tpm2.git && cd tpm2
npm install && npm run build
cargo test --lib -- --skip hw_
npm run verify:package
node examples/smoke-test.mjs runtime
Docs: getting-started.md · windows-pcp.md · roadmap.md · RELEASE.md · SECURITY.md
Low-level Rust validation: cargo run --no-default-features --features probe-bin --bin tbs-probe -- (repo only, not published to npm).
License
Apache-2.0