API reference
Every module Attestium exports, with each function's signature, parameters, return value and errors, and a short example per module. Types are in lib/index.d.ts. All modules are CommonJS; functions that do input or output return promises, the rest are synchronous.
Importing#
The main export is the Attestium class. Every module is also a property of it, and most have their own subpath:
const Attestium = require('attestium');
const {evidence, ProcessIntegrity, Tpm} = require('attestium');
const sigstore = require('attestium/sigstore');| Subpath | Property of the main export | Contents |
|---|---|---|
attestium | The Attestium class | |
attestium/evidence | evidence | Evidence format |
attestium/schema/evidence.schema.json | The JSON Schema | |
attestium/schema | schema | JSON Schema validator |
attestium/util | util | Canonical JSON, hashing, validation |
attestium/file-tree | fileTree | Directory hashing |
attestium/signing | signing | Ed25519 envelopes |
attestium/http | http | Hardened HTTPS GET |
attestium/process-integrity | ProcessIntegrity | Running processes |
attestium/runtimes | runtimes | Language runtime profiles |
attestium/release-verification | ReleaseVerification | Node.js releases and npm packages |
attestium/ecosystems | ecosystems | Package ecosystems |
attestium/elf | elf | Go and Rust build information |
attestium/containers | containers | Containers, attester side |
attestium/oci | oci | Container images, verifier side |
attestium/distro | distro | Debian and Ubuntu packages |
attestium/checksums | checksums | Published checksum lists |
attestium/sigstore | sigstore | Sigstore bundles |
attestium/tuf | tuf | TUF client |
attestium/attestations | attestations | GitHub attestations, npm provenance |
attestium/git-trees | gitTrees | Git trees of pinned commits |
attestium/tpm | Tpm | TPM 2.0 |
attestium/tpm-identity | tpmIdentity | EK certificates, credential activation |
attestium/ima | ima | IMA log |
attestium/confidential | confidential | SEV-SNP and TDX |
attestium/monitor | monitor | eBPF monitor |
attestium/zip, attestium/toml, none for asn1 | zip, toml, asn1 | Parsers |
Some modules export more names than listed here (for example ProcessIntegrity.parseMapsLine). Those are helpers the modules and tests use; they are not described.
The examples run in a directory where npm install attestium has run, and where ./app is a small project directory.
The Attestium class#
File manifests of a project directory, signed baselines, a challenge-response for remote verifiers, runtime tracking of CommonJS modules, continuous re-verification, and TPM-bound reports. It is independent of the evidence format: use it to check one directory on the machine itself, or to answer a verifierThe component that runs elsewhere, typically in CI: it sends a nonce, obtains references itself and compares. with a signed tree digest.
new Attestium(options?)#
| Option | Default | Meaning |
|---|---|---|
projectRoot | process.cwd() | Directory to hash. Throws if it does not exist. |
includePatterns | Attestium.DEFAULT_INCLUDE | Globs of files to include (source, assets, docs) |
excludePatterns | Attestium.DEFAULT_EXCLUDE | Globs to exclude (node_modules, VCS directories, keys, .env*, editor files) |
enableGitignoreInheritance | false | Also exclude the root .gitignore patterns (negations are skipped) |
signingKey | none | Ed25519 private key (PEM, DER or KeyObject) for signed exports and responses. Throws if it is not Ed25519. |
enableRuntimeHooks | false | Record CommonJS modules as they are compiled |
continuousVerification | false | Start periodic re-verification |
verificationInterval | 60000 | Milliseconds, or 'random' (15 to 120 seconds) |
customCategories | {} | {name: RegExp} file categories, checked first |
gitCommit, deployTime | GIT_COMMIT, DEPLOY_TIME environment | Recorded in reports |
enableTpm | true | Allow TPMTrusted Platform Module: a chip with keys that cannot be exported and registers (PCRs) that can be extended but never set. use when one is available |
tpm | {} | Options for Tpm |
logger | console | Object with a log(message) method |
Options can also come from a data-only file in the project root: .attestiumrc, .attestiumrc.json, .attestiumrc.yaml, .attestiumrc.yml, attestium.config.json, attestium.config.yaml, attestium.config.yml, or an attestium key in package.json. Constructor options win. JavaScript configuration files are never loaded.
Reports and baselines#
generateVerificationReport(): Promise<VerificationReport>hashes every included file. Returns{timestamp, attestiumVersion, projectRoot, gitCommit, deployTime, files, errors, digest, summary}. Each file is{relativePath, checksum, gitBlobId, mode, size, category, verified: true}.digestisfileTree.manifestDigest()of the files. Symbolic links are recorded, never followed.verifiedFilescounts files read and hashed; comparing them is a separate step.exportVerificationData(): Promise<Baseline>returns{metadata, files, summary, digest}and, with asigningKey, asignature{alg, keyId, publicKey, value}over the rest.compareWithBaseline(baseline, {publicKey}?): Promise<{valid, signature, added, removed, modified, errors}>re-hashes the tree and compares. WithpublicKey, the baseline must be signed by that key. Without it, a signed baseline is checked only for self-consistency (signature.trustedisfalse): always pass the key you trust.validis true only when nothing was added, removed, modified or unreadable, and the signature (if checked) verifies. Returns{valid: false, errors: [{error: 'Malformed baseline'}]}for a baseline withoutfiles.verifyImportedData(baseline, options?): Promise<boolean>iscompareWithBaseline(...).valid, with a log line on mismatch.scanProjectFiles(): Promise<string[]>returns absolute paths of the included files.calculateFileChecksum(file),generateFileChecksum(file): Promise<string>return a file's SHA-256. They reject for symbolic links (ELOOP) and unreadable files.verifyFileIntegrity(file): Promise<{checksum, verified, timestamp, category?, size?, error?}>never rejects;verifiedis false with anerrorcode.shouldInclude(path),shouldExclude(path),matchesPattern(path, glob),categorizeFile(path)apply the patterns and categories (source,test,config,documentation,static_asset,dependency, or a custom one).parseGitignorePatterns(text): string[]andloadGitignorePatterns()convert.gitignorelines to globs.
Challenge-response#
generateVerificationResponse(nonce): Promise<SignedEnvelope>answers a verifier's nonceA random value the verifier chooses for one request, so that an old answer cannot be replayed. with{type: 'attestium-verification-response', nonce, timestamp, attestiumVersion, gitCommit, digest, summary}, signed withsigningKey. Without a key it returns{payload, signature: null}. ThrowsTypeErrorunless the nonce is 16 to 64 bytes of hex.Attestium.verifyVerificationResponse(response, {nonce, publicKey, digest?, maxAgeMs?}): {valid, errors}checks the signature with the trusted key, the payload type (attestium-verification-response, so another statement signed by the same key is refused), the nonce, the age (default at most 300000 ms old, at most 60 seconds in the future) and, when given, the tree digest.- Carry the nonce and the response over SSH, with the verifier's key restricted to the attesterThe component on the machine being checked. It collects facts and reports them; it never decides the result. command and the server's host key pinned;
examples/ssh-verification.jsdoes both halves. The signature proves only that the holder of the signing key answered: a key on the verified machine is readable by its root user, who can sign any digest (Forged answers). generateChallenge(ttlMs = 300000): {nonce, timestamp, expiresAt},validateChallenge(challenge): booleanandverifyChallenge(challenge, nonce): Promise<boolean>manage challenges on the verifier side.
TPM-bound reports#
isTpmAvailable(): Promise<boolean>is false whenenableTpmis false or no TPM answers.initializeTpm(): Promise<{handle, publicKey, keyId}>returns the persistent AKAttestation key: a key held by the TPM that signs quotes. The verifier pins it at enrollment., creating it on first use. Throws when the TPM is disabled.generateHardwareAttestation(nonce, {pcrList}?): Promise<HardwareAttestation>returns{type: 'hardware-backed', nonce, reportDigest, softwareVerification, hardwareAttestation, timestamp}: a verification report and a TPM quote with qualifying dataSHA-256(nonce ‖ report digest). Throws when no TPM is available.Attestium.verifyHardwareAttestation(attestation, {nonce, publicKey, expectedPcrs?}): {valid, errors}recomputes the report digest from the file list, checks the nonce, and verifies the quote withTpm.verifyQuote().generateHardwareRandom(length = 32): Promise<{bytes, source: 'tpm' | 'os'}>returns TPM random bytes, or the operating system's when no TPM answers.getTpmInstallationInstructions(): string,getSecurityStatus(): Promise<Object>.
Runtime tracking and continuous verification#
setupRuntimeHooks()records the SHA-256 of every CommonJS module's source as it is compiled (installed once per process; ES modules are not covered) and emitsmoduleLoaded.getRuntimeVerificationStatus(): Promise<{enabled, totalModules, changedOnDisk, modules}>lists the recorded modules, each withdiskSha256andchangedOnDisk.startContinuousVerification(interval?),stopContinuousVerification()re-hash the tree periodically (the timer does not keep the process alive).runVerificationCycle(): Promise<violations[]>runs one cycle; the first records the baseline.cleanup(): Promise<void>stops timers and detaches runtime listeners.
Events: integrityViolation ({type: 'fileChanged' | 'fileAdded' | 'fileRemoved', file, ..., timestamp}), fileChanged (file, previousChecksum, newChecksum), moduleLoaded ({filename, sha256, loadedAt}), verificationError (error).
Statics: Attestium.VERSION, Attestium.DEFAULT_INCLUDE, Attestium.DEFAULT_EXCLUDE, Attestium.digestOf(value) (same as util.digestOf).
const Attestium = require('attestium');
(async () => {
// Sign the baseline in CI; keep the private key off the verified machine.
const {publicKey, privateKey} = Attestium.signing.generateKeyPair();
const signer = new Attestium({projectRoot: './app', signingKey: privateKey, logger: {log() {}}});
const baseline = await signer.exportVerificationData();
// On the machine: compare with the baseline, trusting only publicKey.
const checker = new Attestium({projectRoot: './app', logger: {log() {}}});
const result = await checker.compareWithBaseline(baseline, {publicKey});
console.log(result.valid, result.modified, result.added, result.removed);
// A remote verifier: fresh nonce, signed answer, pinned key, expected digest.
const nonce = Attestium.util.generateNonce();
const response = await signer.generateVerificationResponse(nonce);
console.log(Attestium.verifyVerificationResponse(response, {nonce, publicKey, digest: baseline.digest}));
})();evidence#
The evidence format of SPEC.md.
| Name | Value |
|---|---|
TYPE | 'attestium-evidence' |
VERSION | 2 |
MANIFEST_TYPE | 'attestium-manifest' |
MANIFEST_NAME | '.attestium-manifest.json' |
validateEvidence(value): {valid: boolean, errors: string[]}checks a value againstschema/evidence.schema.json. At most 20 errors are reported, each naming the path ($.services[0].files is required).evidenceDigest(evidence): stringreturns the lowercase hex SHA-256 of the canonical JSON of the evidence withoutevidenceDigest,tpm,imaandconfidential. ThrowsTypeErrorfor values canonical JSON refuses (see util).createManifest(directory, {repository, commit, exclude?}): Promise<Manifest>lists every file of a directory (without.gitand the manifest itself;excludetakes globs) as{type: 'attestium-manifest', version: 1, repository, commit, files: {path: [sha256 or 'symlink:<target>', mode]}}. ThrowsTypeErrorunlessrepositoryisowner/nameandcommitis 40 hex digits, and throws if any file cannot be read.parseManifest(text | Buffer): Manifestparses and checks a manifest's shape. ThrowsSyntaxErrorfor invalid JSON andError('Not an Attestium manifest')for any other shape.
const {evidence} = require('attestium');
(async () => {
const manifest = await evidence.createManifest('./app', {repository: 'example/app', commit: 'a'.repeat(40)});
const parsed = evidence.parseManifest(JSON.stringify(manifest));
console.log(Object.keys(parsed.files));
console.log(evidence.validateEvidence({type: evidence.TYPE, version: evidence.VERSION}).errors.slice(0, 3));
})();schema#
compile(schema, {maxErrors = 20}?): (value) => {valid, errors}compiles the JSON Schema subset the evidence schema uses (see Other languages). Unknown keywords and unresolvable$refs throw when compiling, so a schema cannot silently skip a check.
const {schema} = require('attestium');
const check = schema.compile({type: 'object', required: ['name'], additionalProperties: false, properties: {name: {type: 'string', maxLength: 8}}});
console.log(check({name: 'web-1'})); // {valid: true, errors: []}
console.log(check({name: 'a-very-long-name', port: 1}).errors);util#
canonicalize(value): stringreturns canonical JSON: sorted keys, no whitespace, members withundefinedvalues left out. ThrowsTypeErrorfor non-finite numbers,undefinedin an array, and anything that is not null, a boolean, a number, a string, an array or a plain object.digestOf(value): stringissha256(canonicalize(value)).sha256(data: string | Buffer | Uint8Array): stringreturns lowercase hex.safeEqual(a, b): booleancompares two strings in constant time; false for non-strings or different lengths.generateNonce(bytes = 32): stringreturns random hex.normalizeNonce(nonce): stringreturns the nonce in lowercase. ThrowsTypeErrorunless it is 16 to 64 bytes of hex.normalizePid(pid): stringaccepts a positive integer (number or decimal string) or'self'. ThrowsTypeErrorotherwise, which keeps untrusted input out of/procpaths.exists(file): booleantests bystat(), which honors file capabilities, unlikefs.existsSync()(access()).isPlainObject(value): boolean.parallelMap(tasks: Array<() => Promise<T>>, concurrency): Promise<T[]>runs thunks with a concurrency limit, keeping order.setOwn(object, key, value)sets an own enumerable property, even one named__proto__. Use it to build file maps from untrusted paths.
const {util} = require('attestium');
console.log(util.canonicalize({b: 1, a: [true, null, 'x'], c: undefined})); // {"a":[true,null,"x"],"b":1}
console.log(util.digestOf({b: 2, a: 1}) === util.sha256('{"a":1,"b":2}')); // true
const files = {};
util.setOwn(files, '__proto__', 'a file name');
console.log(Object.keys(files)); // ['__proto__']
try {
util.normalizeNonce('abc');
} catch (error) {
console.log(error.message); // Nonce must be 16 to 64 bytes of hex
}fileTree#
walkTree(root, {exclude?, concurrency = 16, root?, rootOwnedLinks?, hash = true}?): Promise<{entries, directories, errors}>walks a directory without following symbolic links. On Linux each directory is entered through its parent's open descriptor, so a directory replaced by a link during the walk is not followed.exclude(relativePath, isDirectory)returns true to skip. Each entry is{path, type: 'file' | 'symlink', mode: '100644' | '100755' | '120000', size, sha256, gitBlobId, ctimeMs, mtimeMs, target?}; a symbolic link's hashes are of its target text. Withhash: false, an entry is only{path, type, ctimeMs, mtimeMs}, and no file or link is read.directorieslists each directory the walk listed as{path, ctimeMs, mtimeMs}, with.for the top: adding, removing or renaming an entry changes both times, so a file added and removed again still shows there. Unreadable entries, and paths longer thanPATH_MAX, go toerrorsas{path, error}with the error code; a FIFO or device is never opened and goes toerrorswithENOTFILE, since a program may still read or load it. A socket has no content and is left out. With the optionroot, the directory is found inside that root asopenInRootfinds it.hashFile(file, {root?, rootOwnedLinks?, inode?}?): Promise<{sha256, gitBlobId, size}>hashes a regular file in chunks. Opens without following symbolic links (rejects withELOOP) or waiting on a FIFO, rejects anything but a regular file, and rejects if the file changes size while it is read. Withroot, the file is found inside that root (seeopenInRoot); withinode, it must be that inode.openInRoot(root, file, {flags?, rootOwnedLinks?}?): number(Linux) opens a file inside a root directory (such as/proc/<pid>/root) the way a process with that root would find it: symbolic links, absolute or relative, and..resolve inside the root. Each step starts from an open directory, so a directory replaced by a link meanwhile is not followed. WithrootOwnedLinks, only links owned by root are followed. Returns a file descriptor.gitBlobId(content): stringreturns git's SHA-1 blob id.manifestDigest(entries): stringreturns SHA-256 over the sorted linespath\0sha256\n(with\0symlinkbefore the newline for links); independent of order, modes and times.globToRegExp(glob): RegExp:**/matches zero or more directories,**anything,*anything but/,?one character but/.createMatcher(globs): (path) => booleanmatches any of the globs.toPosixRelative(root, fullPath): string.
const {fileTree} = require('attestium');
(async () => {
const skip = fileTree.createMatcher(['**/*.log', 'tmp/**']);
const {entries, errors} = await fileTree.walkTree('./app', {exclude: relative => relative === '.git' || skip(relative)});
console.log(entries.map(entry => `${entry.mode} ${entry.path}`), errors);
console.log(fileTree.manifestDigest(entries));
console.log(fileTree.gitBlobId('hello\n')); // ce013625030ba8dba906f756967f9e9ca394464a
})();signing#
generateKeyPair(): {publicKey, privateKey}returns an Ed25519 key pair in PEM.sign(payload, privateKey): {alg: 'ed25519', keyId, publicKey, payload, signature}signs the canonical JSON ofpayload. Throws for keys that are not Ed25519 and for payloads canonical JSON refuses.verify(envelope, trustedPublicKey?): {valid, trusted, keyId, error?}never throws. WithouttrustedPublicKey, it verifies with the envelope's own key andtrustedisfalse.fingerprint(key): stringreturns the SHA-256 of the public key's SPKI DER encoding (thekeyId).toPublicKey(key): KeyObject,toPrivateKey(key): KeyObjectconvert and check the algorithm.ALGORITHMis'ed25519'.
See the example in Signatures.
http#
httpGet(url, options?): Promise<Buffer>fetches over HTTPS (plain HTTP only forlocalhost,127.0.0.1and::1, or withallowHttp: true). Options:headers,timeout(idle, default 30000 ms),deadline(the whole response, however steadily it arrives, default 3600000 ms),maxBytes(default 256 MiB),maxRedirects(default 5),maxRetries(default 3),retryDelay(default 1000 ms, doubled per retry),ca(extra trusted CAs),allowHttp,denyPrivateAddresses,lookup(a resolver withdns.lookup's signature). WithdenyPrivateAddresses: true, no connection reaches a private address (isPrivateAddress): an IP literal host is refused, and a host name is refused when any address it resolves to is private, checked by the lookup the connection itself uses (so DNS rebinding cannot reach one), on the first request and on every redirect; the error'scodeisEPRIVATEADDRESS. Use it for hosts named by evidence. Redirects never go from HTTPS to HTTP and never forwardauthorization,cookieorproxy-authorizationto another host. Retries 429, 5xx, timeouts, refused and reset connections, honoringRetry-After. Rejects witherror.statusCodeset for other statuses (HTTP 404 fetching ...), and on oversize bodies.httpGetJson(url, options?): Promise<any>also sendsaccept: application/jsonand parses the body.assertAllowedUrl(url: URL, {allowHttp, denyPrivateAddresses}?)throws for URLshttpGetrefuses.isPrivateAddress(address): booleanis true for loopback, private (RFC 1918), carrier-grade NAT (100.64.0.0/10), link-local (including169.254.169.254), unspecified, IETF-reserved, benchmarking, multicast and reserved IPv4 addresses, for IPv6 unspecified, loopback, unique-local, link-local, site-local, multicast and local-use NAT64 addresses, for IPv4 addresses inside IPv6 ones (mapped, compatible, NAT64, 6to4) that are, and for anything that is not an IP address.privateAddressLookup(resolve = dns.lookup)returns a lookup that fails withEPRIVATEADDRESSfor a name resolving to any private address;connectOptions({denyPrivateAddresses, lookup})returns thelookupandagentoptions forhttp.requestthat enforce it (no shared agent, so no pooled socket is reused).
const {http} = require('attestium');
(async () => {
const {dist} = await http.httpGetJson('https://registry.npmjs.org/is-number/7.0.0');
console.log(dist.integrity);
await http.httpGet('https://registry.npmjs.org/-/no-such-package-here').catch(error => console.log(error.statusCode));
})();ProcessIntegrity#
Inspects a running process rather than the files it was started from. Linux is fully supported through /proc; on macOS and Windows, checks their tooling cannot perform return supported: false.
new ProcessIntegrity(options?)#
| Option | Default | Meaning |
|---|---|---|
expectedLibs | none | Allowed shared libraries; others are reported as unexpected-lib. Empty means no allowlist. |
maxAnonExecRegions | 512 | Anonymous executable regions (JIT code) above this are reported |
inspectorPorts | [] | Ports that count as an open debugger, besides the runtime's defaults (9229 for Node.js) |
timeout | 10000 | Timeout of platform commands, in ms |
procRoot | '/proc' | Where procfs is mounted |
ldPreloadPath | '/etc/ld.so.preload' | The dynamic linker's preload file |
platform, run | process.platform, execFileSync | For other platforms' tooling and tests |
Methods#
Every method takes a process id (number, decimal string or 'self') and throws TypeError for anything else.
checkAll(pid, {isNode?, runtime?}?): ProcessReportruns every check and returns{pid, platform, timestamp, runtime, memoryMaps, executablePages, linkerIntegrity, tracer, fileDescriptors, listeningSockets, inspectorPorts, findings, incomplete, passed}.findingsare{check, type, severity: 'critical' | 'warning' | 'info', detail}.incompletelists checks that could not run ({check, error}), including executable regions that could not be compared.passedis true only with no critical finding and nothing incomplete.checkMemoryMaps(pid)reports anomalies:deleted-backing(an executable mapping whose file was deleted) andreplaced-backing(its path now names a different file), both warnings because they usually mean an upgrade without a restart;memfd-exec(fileless code),file-wx(a writable and executable file mapping),unexpected-lib. Returns{supported, regions, anomalies, summary, libraries}.checkExecutablePages(pid)compares every file-backed executable mapping in memory with the same bytes of the file, read through/proc/<pid>/rootfor processes in containers. Returns{supported, matched, regions, mismatched, skipped, error?}. A mismatch iscode-modified-in-memory, critical.checkLinkerIntegrity(pid, {runtime?, isNode?}?)checks the dynamic linker's injection vectors (LD_PRELOAD,LD_AUDIT,/etc/ld.so.preload,DYLD_INSERT_LIBRARIES,AppInit_DLLs) and the runtime's (see runtimes). Returns{supported, clean, findings, runtime, inspectorPorts, pm2?, cmdlineRewritten?, ...}.checkTracerPid(pid):{supported, traced, tracerPid}; a tracer isdebugger-attached, critical.checkFileDescriptors(pid):{supported, totalFds, suspicious}; an openmemfdismemfd-open, critical.checkListeningSockets(pid):{supported, listening: [{address, port}]}; a listening inspector port isinspector-listening, critical.detectRuntime(pid, {libraries?, nodeRelease?}?):{name, label, version, by}.getProcessInfo(pid):{pid, supported, name, ppid, uid, startTimeMs, cmdline, exe, exeDeleted?, cwd}. Throws when the process does not exist.listProcesses({uid?, cwdPrefix?, exe?}?): process infos sorted by pid; Linux only, empty elsewhere.
Statics: ProcessIntegrity.findNodeInjectionFlags(args, {hasScript = true}?) returns {preloads, inspector, ports} found in Node.js arguments; parsePm2Environment(json) and parsePm2Fields(fields) read the options PM2 started an application with.
Reading another process's memory needs ptrace access to it; for another user's process, also CAP_DAC_READ_SEARCH.
const {ProcessIntegrity} = require('attestium');
const inspector = new ProcessIntegrity();
const report = inspector.checkAll(process.pid);
console.log(report.runtime, report.passed, report.incomplete);
for (const finding of report.findings) {
console.log(finding.severity, finding.type, finding.detail);
}
// The arguments after the executable name.
console.log(ProcessIntegrity.findNodeInjectionFlags(['--require', './hook.js', 'server.js']));
// {preloads: ['--require ./hook.js'], inspector: [], ports: []}runtimes#
Profiles of language runtimes: how to detect them and their code-loading vectors. Detection uses the executable's name and the mapped libraries; nothing is executed.
PROFILES:node,python,jvm,ruby,dotnet,beam,php,perl,deno,bun, each withname,label, detection patterns and default debug ports, and fordotnetandbeamthe names of the memfds their JIT creates (memfds).NATIVEis the profile for everything else.runtimeMemfd(runtime, target): string|nullsays what a memfd (/memfd:<name>) is when the runtime's profile names it:doublemapperfordotnet(W^X double mapping),vmemforbeam(the JIT's dual mapping).ProcessIntegrityreports those asmemfd-execandmemfd-openwith severityinfo; any other memfd, or those names in another runtime, stayscritical.bundlerSetup(environment): {lib, version}|nullrecognizes exactly theRUBYOPTandRUBYLIBthatbundle execsets:RUBYLIBone.../gems/bundler-<version>/libdirectory, andRUBYOPTonly-r<that directory>/bundler/setupor-rbundler/setup. The Ruby profile then reportsbundler-setupandbundler-rubylib(info) instead ofRUBYOPT-requireandRUBYLIB; a verifier should compare that gem directory with the published gem (rubygems.compareGemDirectory).standardBundlerSetup(environment, exe): {setup}|nullrecognizesbundle execwith the Bundler in the interpreter's standard library (Debian, Ubuntu):RUBYLIBempty, andRUBYOPTonly-r<prefix>/lib/ruby/<version>/bundler/setupfor the interpreter<prefix>/bin/ruby. The Ruby profile then reportsbundler-setup(info) alone.detectRuntime({exe, libraries = [], nodeRelease = false}): {name, label, version, by}.byislibrary,executable,releaseordefault. A library match wins over the executable name.inspectRuntime(runtime, {environment, cmdline, exe?, duplicates?, raw?, parentIsPm2?}): {findings, ports, extra}checks a process's environment and command line for its runtime's injection vectors and the dynamic linker's.parseEnviron(text): {values, duplicates}parses/proc/<pid>/environ, keeping the first of duplicate names and listing them.splitOptions(text): string[]splits an options string asNODE_OPTIONSis split.findNodeInjectionFlags,parsePm2Environment,parsePm2Fields: see ProcessIntegrity.
const {runtimes} = require('attestium');
console.log(runtimes.detectRuntime({exe: '/usr/bin/java', libraries: ['/usr/lib/jvm/java-21-openjdk/lib/server/libjvm.so']}));
console.log(runtimes.parseEnviron('PATH=/usr/bin\0NODE_OPTIONS=--require x\0PATH=/tmp\0'));
const {findings} = runtimes.inspectRuntime('node', {environment: {NODE_OPTIONS: '--require /tmp/x.js'}, cmdline: ['node', 'server.js']});
console.log(findings);ReleaseVerification#
The Node.js binary against the official release, and npm packages against lockfile-pinned registry tarballs. Collection and appraisal are separate, so a verifier can appraise what another machine collected.
new ReleaseVerification(options?)#
| Option | Default | Meaning |
|---|---|---|
projectRoot | process.cwd() | Project with node_modules and a lockfile |
nodeDistUrl | 'https://nodejs.org/dist' | Node.js releases |
registryUrl | 'https://registry.npmjs.org' | npm registry |
githubArchiveUrl | 'https://codeload.github.com' | Archives of GitHub commits |
cacheDir | none | Cache of verified reference manifests |
nodeKeyring | none | Keyring of Node.js release keys; when set, SHASUMS256.txt must be signed (checked with gpgv) |
timeout, maxRetries, retryDelay | 30000, 3, 1000 | HTTP options |
concurrency | 8 | Parallel downloads |
Methods#
The verify* methods never reject: failures are in details.error and passed is false.
verifyNodeRelease({execPath, version, platform, arch, sha256}?): Promise<{name: 'node-release', passed, details}>compares a Node.js binary (default: this process's) withbin/nodein the official archive (node.exefromSHASUMS256.txton Windows). Passsha256to check a hash from evidence without the file.verifyModules(): Promise<CheckResult>compares the project'snode_moduleswith its lockfile.verifyGlobalPackage(dir, {node}?): Promise<CheckResult>checks a global package (npm, pm2) and everything under it; npm and corepack are compared with the copies in the official Node.js archive.verifyAll({checkNode, node, globalDir, globalPackages, modules}?): Promise<{timestamp, platform, arch, nodeVersion, checks, passed, summary}>runs the Node.js, global package (npm,pnpm,pm2by default) and module checks.scanInstalledPackages(nodeModulesDir?, {includeFiles}?): Promise<{packages, unaccounted, links, packageLinks, caches, errors}>hashes every installed package (npm and pnpm layouts). Each package is{name, version, path, digest, fileCount, files?, invalid?};includeFiles(boolean or predicate) adds per-file hashes.linkslists links that do not resolve to an installed package ({path, target, problem}),packageLinksthe others ({path, target}, the target a package'spath).comparePackages({installed, references?, policy?, manifestProvider?, resolveFiles?}): Promise<{passed, summary, findings, files}>appraises scanned packages. Withoutreferences, each package is looked up in the registry.filesmaps the path of each package that matched its reference (verified, patched or built) to the SHA-256 of each of its files, taken from the reference when the package matched by digest, so other records of those files (an IMAIntegrity Measurement Architecture: the Linux kernel hashes files as they are used, logs each hash and extends PCR 10. log) can be compared with the reference rather than with the attester's report. References with apath(frompackage-lock.json) bind each installed package to the one pinned at its path.readLockfile(root?): {format, lockfileVersion, packages}readspnpm-lock.yamlorpackage-lock.json; throws when there is neither.readPackagePolicy(root?): {patched, built}reads pnpm patches andonlyBuiltDependenciesfrompackage.json.getNodeShasums(version): Promise<Map<file, sha256>>,getOfficialNodeBinary({version, platform, arch}),getOfficialNodeArchive({version, platform, arch}, shasums?)fetch and check release files. They throw for malformed versions, missing entries and hash mismatches.getRegistryReference(name, version): Promise<{integrity, tarball}>,getPackageManifest(reference): Promise<PackageManifest>,getPackageFiles(reference, paths): Promise<Map<path, Buffer>>fetch npm references. A tarball that does not match its integrity rejects with... does not match integrity.
Statics: parseLockfile(text, 'pnpm' | 'npm') ({format, lockfileVersion, packages, links}, links being what each link name may resolve to: {importer, owners, declared}), parseShasums(text): Map, githubCommitOf(source), filesNeeded(lock, policy), retargetedLinks(packageLinks, packages, links?): string[] (links pointing to a package other than the one the lockfile resolves the name to), foreignCacheFiles(caches), verifyIntegrity(data, sri): boolean, filesTouchedByPatch(patch), packageManifestFromFiles(files), nodeReleaseTarget(platform, arch), isValidPackageName(name), globalModulesDir(execPath?, platform?).
const crypto = require('node:crypto');
const {ReleaseVerification} = require('attestium');
const data = Buffer.from('tarball bytes');
const sri = `sha512-${crypto.createHash('sha512').update(data).digest('base64')}`;
console.log(ReleaseVerification.verifyIntegrity(data, sri)); // true
console.log(ReleaseVerification.parseShasums(`${'ab'.repeat(32)} node-v22.0.0-linux-x64.tar.gz\n`));
console.log(ReleaseVerification.githubCommitOf(`https://codeload.github.com/owner/repo/tar.gz/${'c'.repeat(40)}`));ecosystems#
Installed-package plugins npm, pypi, rubygems, hex, composer, maven, nuget (also in INSTALLED), and compiled-in go and cargo (also in COMPILED). See Ecosystems and references for what each reads and compares.
Plugin shape (installed ecosystems):
name,label,lockfiles: identification and the lockfile names looked for.detect(root): string[]returns install directories under a project root.installRoot(dir): stringreturns the directory the install occupies.scan(dir, {root}?): Promise<Scan>returns{packages, unaccounted, links, caches, errors, meta?}.readLock(repoDir, {lockfile}?)reads the lockfile (lockfile: a path relative to the repository, for a lockfile not in its default place); throwsNoLockfileErrorwhen there is none. npm reads the first ofpnpm-lock.yaml,npm-shrinkwrap.jsonandpackage-lock.json, and the pnpm policy from thepackage.jsonnext to the lockfile.compare({scan, lock, store, release?, gitTrees?, covered?}): Promise<{passed, summary, findings, issues}>.lockmay benull(every package then fails). A scan witherrors(files it could not read) does not pass.releaseis aReleaseVerification(npm).gitTreesis aGitTrees(Composer; default: one understore.cacheDir).covered(file)returns true for generated files another reference, such as a reproduced build, verifies (Composer, Maven, NuGet).
Other exports:
detectInstalls(root, names?): Array<{ecosystem, dir, installRoot}>runsdetectof each (or the named) plugin. Throws for an unknown name.new ReferenceStore({cacheDir?, httpOptions?, urls?, concurrency = 8})downloads and caches references for the plugins.get(url, options?)andgetJson(url, options?)fetch with the store's HTTP options;memo(key, compute, {persist = true}?)caches a computed value in memory and, withcacheDir, on disk.urlsdefaults:pypihttps://pypi.org,pypiFileshttps://files.pythonhosted.org,rubygemshttps://rubygems.org,hexhttps://repo.hex.pm,nugethttps://api.nuget.org/v3-flatcontainer,mavenhttps://repo1.maven.org/maven2,packagisthttps://repo.packagist.org,crateshttps://static.crates.io/crates,goproxyhttps://proxy.golang.org.NoLockfileError: thrown byreadLock.compareFiles(installed, expected, {allowExtra?, allowMissing?, equivalent?}?): {modified, missing, added}compares twopath -> sha256maps.collect(results, issues?)builds the common comparison shape from per-package results.passedis false when any issue has a severity other thanwarnorinfo.parseCsv(text): string[][]parses CSV as Python'scsvmodule writes it (a wheel'sRECORD).go.readLock(repoDir, {dir}?):{format: 'go.sum', module, sums}; throwsNoLockfileErrorwithoutgo.mod.go.compareBuildInfo({info, lock, commit?, label?}): a comparison.cargo.readLock(repoDir, {lockfile}?):{format: 'cargo', file, packages}; throwsNoLockfileError.cargo.compareAuditable({packages, lock}): a comparison.
const {ecosystems} = require('attestium');
const result = ecosystems.compareFiles(
{'index.js': 'a'.repeat(64), 'extra.js': 'b'.repeat(64)},
{'index.js': 'a'.repeat(64), 'README.md': 'c'.repeat(64)},
{allowMissing: file => file.endsWith('.md')},
);
console.log(result); // {modified: [], missing: [], added: ['extra.js']}
console.log(ecosystems.detectInstalls('./app'));elf#
goBuildInfo(buffer): {goVersion, path, main, deps, settings, unsupported?} | nullreads Go build information (Go 1.18 and later) from an executable of any format;nullwhen it is not a Go binary.depscarrypath,version,sumandreplace. For Go 1.17 and earlier,unsupportedsays why nothing was read.cargoAuditable(buffer): Array<{name, version, source, kind, root}> | nullreads the crate list cargo-auditable embeds in an ELF file;nullwhen there is none. Throws for malformed data.parseElf(buffer),sectionData(buffer, elf, name)read ELF headers and sections.
const fs = require('node:fs');
const {elf} = require('attestium');
console.log(elf.goBuildInfo(fs.readFileSync(process.execPath))); // null: Node.js is not a Go binary
console.log(elf.cargoAuditable(fs.readFileSync(process.execPath))); // nullcontainers#
Attester side. Linux only.
containerOf(pid, procRoot = '/proc'): {id, runtime} | nullfinds the container from the process's cgroup:docker,containerd(including Kubernetes),cri-o,podman.inspectDocker(id, socketPath = '/var/run/docker.sock'): Promise<{name, image: {reference, id, manifestDigest, repoDigests}, platform: {os, architecture}, labels}>asks the Docker Engine API.inspectCri(id, {crictl = 'crictl'}?): Promise<...>asks a CRI runtime throughcrictl inspect; addspodfor Kubernetes.walkRootfs(pid, {procRoot, maxFiles = 500000}?): Promise<{files, fileCount, errors, truncated?, mounts}>hashes every file of the process's root filesystem through/proc/<pid>/root, leaving out other mounts.filesmaps paths (without a leading/) to[sha256 or 'symlink:<target>', mode].walkUpper(upper): Promise<{files, deleted, errors}>walks an overlayfs upper directory: files written, and whiteouts (deleted files).parseMountinfo(text),externalMounts(mounts)(mounts that bring files from outside the image:{destination, source, root, fsType, readOnly}),rootOverlay(mounts).
const fs = require('node:fs');
const {containers} = require('attestium');
console.log(containers.containerOf(process.pid)); // null outside a container
const mounts = containers.parseMountinfo(fs.readFileSync('/proc/self/mountinfo', 'utf8'));
console.log(containers.externalMounts(mounts).slice(0, 3));oci#
Verifier side: images from an OCIOpen Container Initiative: the standard image and registry formats. Manifests and layers are addressed by SHA-256 digest. registry, by digest.
parseReference(text): {registry, repository, tag, digest}normalizes Docker Hub names (nginxisdocker.io/library/nginx). Throws for invalid references.new Registry({httpOptions?, endpoints?, credentials?}?).endpointsmaps a registry name to a base URL (Docker Hub ishttps://registry-1.docker.io);credentialsmaps it to{token}or{username, password}. Anonymous pulls use the registry's token flow. A registry with no configured endpoint (a name taken from evidence), and the token service it names, is never reached at a private address unlesshttpOptions.denyPrivateAddressesisfalse.manifest(reference, digest = reference.digest): Promise<Object>fetches a manifest or index by digest and checks it. Throws for a missing digest or a mismatch.blob(reference, digest, maxBytes = 2 GiB): Promise<Buffer>fetches and checks a blob.platformManifest(reference, {os, architecture, variant?}): Promise<{digest, manifest, index}>resolves an index to the platform's manifest.referrerBundles(reference, digest): Promise<Object[]>returns SigstoreA public signing service: short-lived certificates from Fulcio tied to an OIDC identity, with signatures logged in Rekor. bundles attached as OCI referrers (empty when the registry has none).
imageFiles(registry, reference, manifest, {maxBytes?}?): Promise<Map<path, [sha256 or 'symlink:<target>', mode]>>applies the layers in order, whiteouts included. Layers are held in memory; an image whose layers together expand beyondmaxBytes(default 4 GiB) is refused.compareRootfs(actual, expected, {ignore}?): {modified, missing, added, modeChanged}.decompressLayer(blob, mediaType, maxBytes?)(refuses a layer expanding beyondmaxBytes, default 4 GiB),applyLayers(tars).
const {oci} = require('attestium');
console.log(oci.parseReference('nginx:1.27'));
// {registry: 'docker.io', repository: 'library/nginx', tag: '1.27', digest: null}
const expected = new Map([['bin/sh', ['a'.repeat(64), '100755']], ['etc/os-release', ['b'.repeat(64), '100644']]]);
console.log(oci.compareRootfs({'bin/sh': ['a'.repeat(64), '100755'], 'tmp/x': ['c'.repeat(64), '100644']}, expected));
// {modified: [], missing: ['etc/os-release'], added: ['tmp/x'], modeChanged: []}distro#
Debian and Ubuntu packages.
new DpkgDatabase({root = '/'}?):available(): boolean;ownerOf(file): {name, version, arch, source, listedAs, installedAt} | null(merged-/usraliases are tried;installedAtis the time of the package's file list).new ArchiveReference({archives, store, gpgv = 'gpgv', dpkgDeb = 'dpkg-deb'}):files(name, version, arch, installedAt?): Promise<{path: sha256 or 'symlink:<target>'}>returns the files of a published package (absolute paths). It verifiesInReleasewith the archive's keyring (refusing a signature by a revoked or expired key, fromgpgv's status lines on descriptor 3), thePackages.gzindex and the.debby their SHA-256. A version not in the archives is looked up in their snapshots nearinstalledAt. Throws withcode: 'ENOTINARCHIVE'when it is found nowhere, and throws when a signature or hash does not match.locate(name, version, arch, installedAt?): Promise<{url, filename, sha256} | null>.
osRelease(root = '/'): {id, versionId, codename} | null.defaultArchives({id, codename}, arch): Archive[]fordebianandubuntu(empty for others). An archive is{url, suites, components, keyring, snapshot?}.parseStanzas(text),aliases(file).
const {distro} = require('attestium');
const release = distro.osRelease();
console.log(release);
const database = new distro.DpkgDatabase();
if (database.available()) {
console.log(database.ownerOf('/bin/sh'));
}
console.log(distro.defaultArchives({id: 'debian', codename: 'bookworm'}, 'amd64').map(archive => archive.url));checksums#
parseChecksums(text): Map<file, sha256>reads GNU and BSD checksum lines; other lines are ignored.fetchChecksums(source, {store, trustedRoot?}): Promise<{checksums, signed}>fetches a list and verifies its signature first.sourceis{url, signature?: {type: 'gpg' | 'minisign' | 'sigstore', url?, keyring?, publicKey?, identity?}};identityis required forsigstore.trustedRootis a function returning the Sigstore trusted root. Rejects when the signature does not verify, for an unknown type, for asigstoresignature withoutidentity, and on HTTP errors.verifyMinisign(data, signatureText, publicKey): boolean.toIdentity(identity): Objectturns/.../strings into regular expressions.
const {checksums} = require('attestium');
const list = checksums.parseChecksums([
`${'ab'.repeat(32)} app-linux-amd64`,
`${'cd'.repeat(32)} *app-windows.exe`,
`SHA256 (app-darwin) = ${'ef'.repeat(32)}`,
].join('\n'));
console.log(list);See Signatures for signed lists.
sigstore#
verifyBundle(bundle, {trustedRoot, identity?, subject?, payloadType?, artifact?, publicKeys?}): {statement, claims, signedAt, keyHint}verifies a Sigstore bundle (v0.1 to v0.3). ThrowsSigstoreErroron any failure. See Signatures for what it checks.loadTrustedRoot(json): {authorities, logs}parses atrusted_root.json.certificateClaims(certificate: X509Certificate): Objectreturns the FulcioSigstore's certificate authority. It issues short-lived signing certificates for OIDC identities. identity claims.pae(payloadType, payload): Bufferis the DSSEDead Simple Signing Envelope: signs a payload together with its type. Used by in-toto and Sigstore attestations. pre-authentication encoding.rootFromInclusionProof(index, size, leafHash, proof),verifyCheckpoint(envelope, log),verifyTimestamp(der, signature, trustedRoot)are the transparency log and timestamp checks.SigstoreError,FULCIO_OIDS.
const {sigstore} = require('attestium');
console.log(sigstore.pae('application/vnd.in-toto+json', Buffer.from('{}')).toString());
// DSSEv1 28 application/vnd.in-toto+json 2 {}
try {
sigstore.verifyBundle({mediaType: 'text/plain'}, {trustedRoot: {}});
} catch (error) {
console.log(error instanceof sigstore.SigstoreError, error.message);
}tuf#
new TufClient({metadataUrl, targetsUrl?, initialRoot, cacheDir?, httpOptions?, now?})implements the TUFThe Update Framework: signed, versioned metadata for distributing keys and files. Sigstore ships its trusted root with it. 1.0 client workflow for a repository with consistent snapshots: root rotation, expiry, rollback, thresholds, lengths and hashes, and one level of delegated targets.nowreturns aDate. WithcacheDir, verified root versions (used only when they chain frominitialRoot) and the newest timestamp and snapshot (for rollback checks) are kept between runs.target(name): Promise<Buffer>downloads a target file, verified against its targets metadata. ThrowsTufError.refresh()runs the update workflow once and caches the result.
canonicalJson(value): the OLPC canonical JSON TUF signs.TufError,verifyThreshold,checkHashes,matchPath.
const fs = require('node:fs');
const path = require('node:path');
const {tuf} = require('attestium');
(async () => {
// The Sigstore root Attestium ships; in production, pin your own copy.
const initialRoot = JSON.parse(fs.readFileSync(path.join(path.dirname(require.resolve('attestium')), 'data', 'sigstore-root.json'), 'utf8'));
const client = new tuf.TufClient({metadataUrl: 'https://tuf-repo-cdn.sigstore.dev', initialRoot});
const trustedRoot = JSON.parse(await client.target('trusted_root.json'));
console.log(trustedRoot.tlogs.map(log => log.baseUrl));
})();attestations.SigstoreTrust wraps this client with the shipped root; prefer it.
attestations#
new SigstoreTrust({cacheDir?, httpOptions?, tufUrl?, initialRoot?, trustedRoot?, npmKeys?}?):trustedRoot(): Promise<Object>andnpmKeys(): Promise<{[hint]: {pem, validUntil}}>, through TUF from the shipped root, or from the given values.githubIdentity({repository, workflow?, ref?, workflowRepository?}): {issuer, subjectAlternativeName: RegExp, sourceRepositoryURI}. The subject alternative name must name the workflow file inworkflowRepository(defaultrepository), and the source repository must berepository: a public repository's reusable workflow can be called from any other repository, whose code it then builds.githubAttestations({repository, digest, httpOptions?, apiUrl?}): Promise<Object[]>lists the bundles GitHub stores for a SHA-256 (empty on 404). ThrowsTypeErrorfor a malformed repository or digest.verifyGithubAttestation({bundles, digest, signer, trust, predicateType?}): Promise<{statement, claims, signedAt}>returns the first bundle that verifies; throwsSigstoreErrorlisting every failure otherwise.npmProvenance({name, version, integrity, trust, registryUrl?, httpOptions?}): Promise<{provenance, published?, repository?, commit?, workflow?, signedAt?}>verifies npm provenance and publish attestations for the tarball a SHA-512 integrity names.snappyDecompress(buffer),GITHUB_ISSUER.
const {attestations} = require('attestium');
const identity = attestations.githubIdentity({repository: 'example/app', workflow: '.github/workflows/release.yml', ref: 'refs/tags/v1.0.0'});
console.log(identity.issuer, identity.subjectAlternativeName.test('https://github.com/example/app/.github/workflows/release.yml@refs/tags/v1.0.0'));See Signatures for complete examples.
gitTrees#
new GitTrees({cacheDir, git = 'git', timeout = 600000, allowFileUrls = false})reads commits of other repositories from blobless clones incacheDir. Git runs with hooks off, no prompts, no system or global configuration, and onlyhttpsURLs (fileonly withallowFileUrls).tree(url, commit): Promise<Map<path, {mode, blob}>>lists every file of the commit's tree.file(url, commit, path): Promise<Buffer | null>returns one file's contents, fetched on demand.
exportIgnore(gitattributesText): (path) => booleanmatches files a.gitattributesmarksexport-ignore.
const {gitTrees} = require('attestium');
(async () => {
const trees = new gitTrees.GitTrees({cacheDir: './git-cache'});
const tree = await trees.tree('https://github.com/jonschlinkert/is-number.git', '98e8ff1da1a89f93d1397a24d7413ed15421c139');
console.log(tree.get('index.js'));
const ignored = gitTrees.exportIgnore('/tests export-ignore\n*.md export-ignore\n');
console.log(ignored('tests/a.js'), ignored('README.md'), ignored('index.js'));
})();Tpm#
Attester side through tpm2-tools; Tpm.verifyQuote() is plain Node.js.
new Tpm(options?)#
| Option | Default | Meaning |
|---|---|---|
tcti | none | TCTI string, for example device:/dev/tpmrm0 or swtpm:host=127.0.0.1,port=2321 |
akHandle | '0x81010002' | Persistent AK handle (0x81xxxxxx); throws TypeError otherwise |
timeout | 30000 | Command timeout, in ms |
devices | ['/dev/tpmrm0', '/dev/tpm0'] | Device nodes checked when no tcti is set |
run | execFile | Command runner, for tests |
Methods#
Commands that fail reject with <tool> failed: <last line of its error output>.
checkAvailability(): Promise<{available, reason?, family?}>,isAvailable(): Promise<boolean>.createAttestationKey({handle?, algorithm = 'rsa' | 'ecc', replace = false}?): Promise<{handle, publicKey, keyId}>creates an AK under the EKEndorsement key: the TPM's identity key, certified by its manufacturer. (RSASSA or ECDSA with SHA-256) and makes it persistent. Rejects when a key exists at the handle, unlessreplace.getAttestationKey(handle?): Promise<{handle, publicKey, keyId}>reads the AK's public key (PEM) and its SPKI SHA-256.getAttestationKeyPublicArea(handle?): Promise<string>returns the AK'sTPM2B_PUBLIC, base64, for enrollment.getEndorsement({algorithm = 'rsa'}?): Promise<{algorithm, publicArea, certificate}>returns the EK public area and the manufacturer's certificate from NV (nullwhen there is none), base64.activateCredential({credential, algorithm = 'rsa', handle?}): Promise<string>recovers a secret made withtpmIdentity.makeCredential(), base64. ThrowsTypeErrorfor anything that is not a credential blob.quote({nonce, pcrs = [0..7], bank = 'sha256', handle?}): Promise<Quote>returns{keyId, handle, hashAlg, message, signature, pcrs}(base64 message and signature).nonceis 1 to 32 bytes of hex; hash longer values first.readPcrs(pcrs = [0..7], bank = 'sha256'): Promise<{index: hex}>.extendPcr(pcr, bank, digest): Promise<void>for application measurements.getRandom(length = 32): Promise<Buffer>, 1 to 64 bytes.
Statics: Tpm.verifyQuote({quote, publicKey, nonce, expectedPcrs?}): {valid, errors, attest, pcrs} (see Hardware), Tpm.parseAttest(buffer), Tpm.parsePcrYaml(text), Tpm.HASH_SIZES.
See Hardware for examples.
tpmIdentity#
Verifier side of TPM enrollment, plain Node.js.
parseTpmPublic(buffer): {type: 'rsa' | 'ecc', nameAlg, attributes, key: KeyObject, name: Buffer, symmetric, curve, raw}parses aTPM2B_PUBLIC. Throws for truncated or unsupported structures.attestationKeyProblems(parsed): string[]lists why a key is not an AK (empty when it is one).makeCredential({ek, akName, secret}): Bufferencrypts a secret (at most 32 bytes) to the EK for the AK's name, in the formattpm2_activatecredentialreads. Supports RSA 2048 and ECC P-256 EKs with AES-128-CFB; throws otherwise.verifyEkCertificate({certificate, ekKey, roots, intermediates = []}): {subject, issuer, chain}checks that a DER EK certificate is forekKeyand chains to one ofroots(X509Certificates), with every CA currently valid and every intermediate a CA allowed to sign certificates. Throws otherwise.kdfa,kdfe,ATTRIBUTE.
See Hardware for an example.
ima#
readLog(file = DEFAULT_LOG): Bufferreads the binary measurement list (/sys/kernel/security/ima/binary_runtime_measurements).parseBinaryLog(buffer, {littleEndian = true}?): Entry[]returns{pcr, templateName, templateDigest, templateData, violation, fileHashAlgorithm?, fileHash?, path?}for each entry. Throws for truncated logs.replay(entries, bank = 'sha256', pcr = 10): stringreturns the PCRPlatform Configuration Register: a TPM register that can only be extended with a hash. Firmware, bootloader and kernel extend PCRs with what they load. value the log produces. Throws for unsupported banks and templates.backedEntries(entries, quoted, bank = 'sha256', pcr = 10): Entry[] | nullreturns the prefix of the log that replays to the quoted value, ornullwhen none does.measurementsByPath(entries, {pcr = 10}?): Map<path, {algorithm, hash, count, hashes}>, skipping other PCRs and violations.
const crypto = require('node:crypto');
const {ima} = require('attestium');
// One ima-ng entry for PCR 10, as the kernel writes it (little-endian).
const u32 = value => {
const buffer = Buffer.alloc(4);
buffer.writeUInt32LE(value);
return buffer;
};
const field = data => Buffer.concat([u32(data.length), data]);
const fileHash = crypto.createHash('sha256').update('file contents').digest();
const templateData = Buffer.concat([field(Buffer.concat([Buffer.from('sha256:\0'), fileHash])), field(Buffer.from('/usr/bin/app\0'))]);
const log = Buffer.concat([u32(10), crypto.createHash('sha1').update(templateData).digest(), u32(6), Buffer.from('ima-ng'), field(templateData)]);
const entries = ima.parseBinaryLog(log);
const pcr10 = ima.replay(entries);
console.log(ima.measurementsByPath(ima.backedEntries(entries, pcr10)));confidential#
TSM_ROOT:/sys/kernel/config/tsm/report.reportData(nonce, digest): BufferisSHA-512(nonce ‖ digest)over the decoded hex.collectReport(reportData, {entry?, root?}?): {provider, report, auxblob}asks the kernel for a report over 64 bytes through configfs-tsm. ThrowsTypeErrorunlessreportDatais 64 bytes, and throws when another request changed the report in between (retry).verifyConfidential({provider, report, auxblob?}, expected, {vcek?, roots?, root?, qeIdentity?}?)verifies ansev_guestortdx_guestreport (base64 fields) overexpected. Returns{type: 'sev-snp', measurement, product, key, tcb, policy, vmpl}or{type: 'tdx', measurement, qeSvn, rtmr, teeTcbSvn, mrConfigId, mrOwner}; throws otherwise.verifySnpReport({report, reportData, vcek?, certificates?, roots?}),verifyTdxQuote({quote, reportData, root?, qeIdentity?})are the two verifiers. A TDXIntel Trust Domain Extensions: confidential VMs whose measurement is signed through Intel's quoting enclave. quote must come from Intel's quoting enclave: its QE report's MRSIGNER, ISVPRODID, attributes and MISCSELECT must matchqeIdentity(defaultTD_QE_IDENTITY, Intel's publishedTD_QEidentity), since the PCK also signs reports of other enclaves allowed the provisioning key.parseSnpReport(buffer),snpProduct(parsed),tcbParts(tcb, product),parseCertificateTable(auxblob),vcekClaims(certificate),vcekUrl(parsed, product, base?),parseTdxQuote(buffer),amdRoots().
const {confidential, util} = require('attestium');
const nonce = util.generateNonce(16);
const digest = util.sha256('evidence');
console.log(confidential.reportData(nonce, digest).length); // 64
console.log(Object.keys(confidential.amdRoots())); // ['Milan', 'Genoa', 'Turin']See Hardware for the attester and verifier.
monitor#
Records programs started and files mapped executable with eBPF (bpftrace), so the next audit can report what ran since the last one.
run({log, maxBytes = 64 MiB, bpftrace = 'bpftrace', mmap = true, maxStrlen = 200, onLine?}): {child, done}runs bpftrace (as root, withBPFTRACE_MAX_STRLEN=maxStrlen) and appends<ms> <exec|mmap> <pid> <uid> <path>lines tolog, rotating it tolog.1atmaxBytes. bpftrace prints each event with a random token before and after the path; the token is new for each run and only in a private file, so a path with line breaks cannot end its event early or add events. In the log, a line break in a path is written\n, a backslash\\, and a final\+marks a path bpftrace cut short (one ofmaxStrlen - 1bytes that is not the running program's path). Without kernel support forfentry, it restarts recording program starts only.doneresolves with bpftrace's exit code.readLog(file, {since?, limit = 2000, maxBytes = 64 MiB}?): {since, until, execs, maps, truncated, malformed}summarizes the log and its rotated predecessor: each distinct path withcount,uids,firstSeen,lastSeen, anderrorfor a path cut short.summarize(lines, {since?, limit?}?),parseLine(line),escapePath(path, cut?),eventReader(onEvent, token),script({mmap, token}?)(the token is printed around each path;run()picks a random one per run and passes the program in a private file, never on the command line).
const fs = require('node:fs');
const {monitor} = require('attestium');
const now = Date.now();
fs.writeFileSync('monitor.log', `${now} exec 42 0 /usr/bin/curl\n${now + 5} mmap 42 0 /usr/lib/libssl.so.3\n`);
console.log(monitor.readLog('monitor.log', {since: now - 60_000}));Parsers#
Building blocks the other modules use, with limits for untrusted input:
zip.listZip(buffer),zip.readZipFiles(buffer, {stripFirstComponent?, maxUncompressedBytes = 1 GiB, filter?}?): Map<path, Buffer>: ZIP archives (jars, wheels,.nupkg), with path checks and a size limit. A member whose local header names it differently, or with another compression method, than the central directory is refused.toml.parseToml(text): Object: TOML (Cargo.lock,uv.lock,pylock.toml); throwsTomlError.asn1.parse(der),asn1.certificateExtensions(der): Map<oid, {critical, value}>, andoid,text,integer,time,content,raw: DER decoding; throwsAsn1Error(also for a certificate with a duplicate or malformed extension, and a time that names no real date).
const {toml} = require('attestium');
console.log(toml.parseToml('[[package]]\nname = "serde"\nversion = "1.0.0"\n'));