Attestium evidence format, version 2

Evidence is what an attesterThe component on the machine being checked. It collects facts and reports them; it never decides the result. reports about a machine so that a verifierThe component that runs elsewhere, typically in CI: it sends a nonce, obtains references itself and compares., running somewhere else, can decide whether the machine runs what it claims to run. This document defines the format. It is independent of any language or transport: an attester or verifier written in another language interoperates by following it.

The machine-readable definition is schema/evidence.schema.json (JSON Schema 2020-12, using a subset that lib/schema.js implements). Where this document and the schema differ, the schema decides the shape and this document decides the meaning.

Roles#

  • Attester: runs on the audited machine, collects facts, and reports them. It never decides whether the machine passes.
  • Verifier: runs elsewhere (typically in CI), sends a fresh nonceA random value the verifier chooses for one request, so that an old answer cannot be replayed., receives evidence, and compares every fact with a reference it obtains itself: a public commit, a reproduced build, a registry package, a container image, an official release, a signed distribution archive.
  • Relying partyWhoever reads the verifier's result, for example on a status page.: reads the verifier's report (for example a status page).

These follow the IETF RATSRemote ATtestation procedureS: the IETF architecture (RFC 9334) that defines attester, verifier and relying party. architecture (RFC 9334): Attestium evidence is RATS evidence, and a verifier's report is an attestation result.

Freshness and binding#

  1. The verifier chooses a random nonce of 16 to 64 bytes and sends it hex-encoded.
  2. The attester collects everything below, then computes evidenceDigest (see "Digest").
  3. When hardware is available, the attester binds the nonce and the digest into hardware-signed statements:
    • TPMTrusted Platform Module: a chip with keys that cannot be exported and registers (PCRs) that can be extended but never set. 2.0: a quote whose qualifying data is SHA-256(nonce || evidenceDigest) (raw bytes, not hex), in the tpm.quote field.
    • Confidential VM (AMD SEV-SNPAMD Secure Encrypted Virtualization with Secure Nested Paging: confidential VMs whose launch measurement the AMD chip signs., Intel TDXIntel Trust Domain Extensions: confidential VMs whose measurement is signed through Intel's quoting enclave.): a report whose 64 bytes of report data are SHA-512(nonce || evidenceDigest), obtained through Linux configfs-tsm, in the confidential field.
  4. The IMAIntegrity Measurement Architecture: the Linux kernel hashes files as they are used, logs each hash and extends PCR 10. log, if any, is read after the quote and is authenticated by replaying it to the quoted PCRPlatform Configuration Register: a TPM register that can only be extended with a hash. Firmware, bootloader and kernel extend PCRs with what they load. 10 in the SHA-256 bank. Only the prefix whose replay equals the quoted value is authenticated; entries after it were measured after the quote and are ignored.

A verifier accepts evidence only if nonce equals the nonce it sent, collectedAt is within its accepted window, and evidenceDigest equals the digest it recomputes.

The nonce makes evidence fresh; collectedAt is the attester's clock. Nothing in software evidence identifies the machine: an attester can forward the nonce to another machine and return that machine's evidence. Only a hardware statement verified with a key pinned for that machine (a TPM attestation key) binds evidence to it. A confidential VM report identifies the launch image, not the instance.

Transport#

This format defines no transport, and no transport makes software evidence true: the attester runs on the audited machine, so whoever controls that machine controls what it reports, including the nonce and the digest. A transport decides only who may ask and which machine answered. Reach the attester through a channel that authenticates both ends and runs nothing but the attester, such as an SSH key restricted to the attester command (restrict,command="...") with the machine's host key pinned by the verifier. Do not serve evidence on a network endpoint: it would show anyone who can reach it the machine's files, packages and processes, and the verifier would trust whatever certificate authority or proxy stands in front of it. Only hardware statements, verified as below, make a forged answer fail.

Digest#

evidenceDigest is the lowercase hex SHA-256 of the canonical JSON encoding of the evidence object without the fields evidenceDigest, tpm, ima and confidential.

Canonical JSON:

  • null, true, false as in JSON.
  • Numbers: finite only, written as ECMAScript JSON.stringify writes them.
  • Strings: as ECMAScript JSON.stringify writes them.
  • Arrays: [ items joined by , ]; an undefined item is an error.
  • Objects: members whose value is undefined are left out; the remaining keys are sorted by UTF-16 code units; { then "key":value pairs joined by , then }.
  • No whitespace anywhere.

Top level#

FieldMeaning
type"attestium-evidence"
version2
nonceThe verifier's nonce, lowercase hex
collectedAtWhen collection started, ISO 8601 UTC
attestername and version of the attester implementation, its platform, arch, runtime (node), and its own executable (path, sha256 or error) so a verifier can match it with a published release
hosthostname, kernel, bootId (from /proc/sys/kernel/random/boot_id), os (id, versionId, codename from os-release)
distro{format: "dpkg", arch} when executables and libraries carry their owning distribution package, else null
servicesWhat runs on the machine; see "Services"
executablesEvery running executable, once; see "Code"
librariesEvery file mapped executable into an inspected process, once
globalPackagesOptional: tools installed next to the Node.js runtime (npm, corepack, pnpm, pm2) with their files
monitorOptional: what was executed or mapped since the previous window, or {error} when the record could not be read; see "Monitor"
evidenceDigestSee "Digest"
tpmOptional: {enabled: false}, {available: false, reason, required}, {available: true, error} or {available: true, quote}; a quote is {message, signature, pcrs, keyId, handle, hashAlg} (base64 TPMS_ATTEST and signature, the quoted PCR values by bank, the attestation key's id and persistent handle, and the signature's hash: sha256, sha384 or sha512)
confidentialOptional: {enabled: false}, {available: false, reason, required}, {available: true, error, required} or {available: true, provider, report, auxblob} (base64)
imaOptional: {log} (base64 binary measurement list) or {error}

required tells the verifier the attester was configured to require the hardware, so its absence is a failure rather than a missing option.

Services#

A service is either a directory or a set of containers.

Directory service (kind: "directory")#

FieldMeaning
nameThe service's name, matching the verifier's configuration
root, realRootThe configured and resolved root
git{commit, ref} read from .git without running git, or {commit: null, error}
filesEvery file under the root except .git and installed package directories: path -> [sha256 or "symlink:<target>", mode], mode being git's 100644, 100755 or 120000
fileCount, truncatedThe number of files found, and whether files was cut at the attester's limit
errorsFiles that could not be read: {path, error}
manifestOptional: the contents (base64) of .attestium-manifest.json, for a release deployed without git; see "Release manifests"
installsInstalled packages, one entry per install directory; see "Installed packages"
processesEvery process whose working directory is inside the root (and whose user matches, when configured); see "Processes"
userProcessesThe same user's other processes, listed but not inspected: {pid, exe, name, cwd} (each but pid may be null)

Container service (kind: "container")#

containers lists every running container matching the service's filter:

FieldMeaning
id, runtime, nameContainer id (hex), runtime (docker, containerd, cri-o, podman), name
imagereference, id, manifestDigest (the platform manifest the runtime recorded), repoDigests (registry/repository@sha256:...)
platformos, architecture (OCIOpen Container Initiative: the standard image and registry formats. Manifests and layers are addressed by SHA-256 digest. names)
mountsMounts that bring in files from outside the image: destination, source, root, fsType, readOnly
upperThe overlay's writable layer: files, deleted (whiteouts), errors
rootfsOptional: every file of the root filesystem (other mounts left out), with fileCount, errors, truncated
processesThe container's processes
errorThe container could not be inspected

Installed packages#

Each entry of installs has ecosystem (npm, pypi, rubygems, hex, composer, maven, nuget), dir (relative to the root when inside it), packages, unaccounted (files that belong to no package), links (links that do not resolve to an installed package: {path, problem}), for npm packageLinks (every link that resolves to an installed package: {path, target}, the target being that package's path; a verifier requires the package the lockfile resolves the link's name to), caches (bytecode caches: {path, files}), errors, and ecosystem-specific meta.

A package has name, version, path (relative to dir), files (path -> sha256, relative to the package), optionally invalid: true and meta (for example a wheel's generated scripts, a gem's specification, the blobs of a Composer package).

An npm package also has digest and fileCount: the lowercase hex SHA-256 of one line per file of the package (its own node_modules and __pycache__ directories left out), path NUL sha256 newline, or path NUL sha256 NUL symlink newline for a symbolic link (hashed as its target), the lines sorted; and the number of files. Its files may then be left out: a verifier compares the digest with the reference's, and needs files for a package it compares file by file (a patched package) or whose files a process maps; when present, files must produce the digest.

The verifier reads the lockfile at the deployed commit, from its own checkout of the public repository, and compares each package with its reference: the registry tarball the lockfile's integrity names (npm), the wheel named by the lockfile's hash through its RECORD (PyPI), the .gem whose checksum Gemfile.lock pins (RubyGems), the Hex tarball by its outer checksum, the git tree of the commit composer.lock pins (with export-ignore), the jar hash Gradle's verification metadata or a Maven lockfile pins, the NuGet package whose content hash packages.lock.json pins.

Processes#

FieldMeaning
pid, ppid, uid, cwd, exe, exeDeletedFrom procfs
cmdlineAt most 32 arguments of at most 512 characters
startTimeISO 8601, or null
runtime{name, label, version, by}: node, python, jvm, ruby, dotnet, beam, php, perl, deno, bun, or native
integrityfindings ({type, severity: critical, warning or info, check, detail}; a verifier treats a finding without severity as a warning), incomplete (checks that could not run), mapped libraries (every file mapped executable; empty when there are none), executable page comparison, dynamic linker state, tracer, suspicious file descriptors, listening sockets
changedAfterStart, metadataChangedAfterStartFiles of the service written, or whose status changed, after the process started; ...Truncated when cut. Optional: an attester that does not track them leaves them out, and a verifier then has nothing to compare
bundlerFor a Ruby process started by bundle exec: the bundler gem directory its RUBYLIB names, {dir, version, files, errors} (files: path -> sha256), for comparison with the published gem

Runtime findings cover each runtime's code-injection vectors: environment variables that load code (NODE_OPTIONS, PYTHONPATH, JAVA_TOOL_OPTIONS, RUBYOPT, DOTNET_STARTUP_HOOKS, ERL_FLAGS, PHP_INI_SCAN_DIR, PERL5OPT, LD_PRELOAD, LD_AUDIT and others), command-line options that do the same, debugger and management ports, and attach mechanisms.

Some values runtimes set themselves are info: the memfd the .NET runtime maps its JIT code from (doublemapper, W^X double mapping) and the BEAM JIT's (vmem), each only in a process of that runtime; and exactly the RUBYOPT and RUBYLIB bundle exec sets (bundler-setup, bundler-rubylib: only Bundler's setup required, and only the bundler gem's lib directory on RUBYLIB), whose gem directory a verifier compares with the published gem.

A critical finding is evidence of tampering; warning deserves attention; info is context. A process with anything in incomplete cannot pass: a clean result means nothing unless every check ran.

Code#

executables and libraries list every file that an inspected process runs or maps executable, once per container (or the host):

FieldMeaning
path, containerThe path as the process sees it, and the container id or null
sha256 or errorThe hash of the running file, read through /proc/<pid>/exe or the process's root
deletedThe running copy was replaced on disk
platform, arch, sizeExecutables only: the attester's platform and architecture (Node.js names), and the file's size in bytes
nodeVersionFor official Node.js builds, the version from the release URL embedded in the binary
goGo build information: goVersion, main, deps (with go.sum hashes and replacements), settings (including vcs.revision, vcs.modified, -trimpath)
cargo{packages} recorded by cargo-auditable
packageThe owning distribution package: name, version, arch, listedAs, source (its source package), installedAt

A verifier explains each file by a reference: a service file that matched its commit, build or attested release; a package file; an image file; an official runtime release; a pinned hash or a signed published checksum list; or the file of the owning package in the distribution's signed archive (a snapshot from the install time for superseded versions). A file no reference explains is reported as unexplained; one that contradicts its reference fails.

The build information inside Go and Rust binaries is written by the build, so it proves nothing about the binary by itself; the binary's own hash does. It shows which dependencies were built in, and those are compared with the lockfile at the commit.

Monitor#

When a kernel-level monitor (eBPF) records program executions and executable mappings, monitor summarizes the window: since, until, execs and maps (path, count, uids, firstSeen, lastSeen, and the file's sha256 and package, or error when it is gone), truncated, malformed. A verifier explains these files like running code; a program that ran and is gone is reported.

Release manifests#

A release deployed without git (a compiled binary, a bundle) carries .attestium-manifest.json at its root:

JSON
{"type": "attestium-manifest", "version": 1, "repository": "owner/name", "commit": "<40 hex>", "files": {"path": ["<sha256 or symlink:target>", "<mode>"]}}

A CI workflow writes it from the build output and attests its SHA-256 (for example with GitHub artifact attestations, a SigstoreA public signing service: short-lived certificates from Fulcio tied to an OIDC identity, with signatures logged in Rekor. bundle signed by the workflow's identity). The verifier checks the attestation's signer (repository, workflow file, ref), that the attestation is for the commit the manifest names, that the commit is on the audited branch of the public repository, and every file against the manifest.

Verifier obligations#

A verifier implementing this format must:

  • check the evidence against the schema before using any field;
  • check the nonce, the time window and the digest;
  • verify hardware statements only with keys it pinned or chains to vendor roots it ships, never with keys taken from the evidence;
  • pin a distinct attestation key for each machine, so that one machine cannot answer for another;
  • decide from its own configuration which hardware statements each machine must provide, and fail when one is missing, rather than accept the lower level the evidence offers;
  • compare the IMA measurements it authenticated with its references, not with the hashes the evidence reports, and report a service none of whose files were measured;
  • obtain every reference itself, by digest where the reference is content-addressed;
  • treat checks that could not complete as inconclusive, never as passing;
  • report the evidence level of every result, since software evidence can be forged by whoever controls the attester.

Versioning#

version changes when a change could make a verifier misread evidence. Objects the schema closes (additionalProperties: false) reject unknown fields, so an attester adds fields there only with a new version; other objects may gain optional fields without one.

Edit this page on GitHub