Other languages

Write an attesterThe component on the machine being checked. It collects facts and reports them; it never decides the result. or a verifierThe component that runs elsewhere, typically in CI: it sends a nonce, obtains references itself and compares. in another language that interoperates with Attestium. The contract is SPEC.md and schema/evidence.schema.json; this page adds what you need to get the canonical JSON, the digest and the hardware binding byte for byte right, lists the checks a verifier must make, and gives test vectors.

What to implement#

You writeYou must produce or check
An attesterEvidence that validates against the schema, with evidenceDigest computed as below, and hardware statements over the binding values below
A verifierSchema validation first, then the nonceA random value the verifier chooses for one request, so that an old answer cannot be replayed., the time window, the digest, the hardware statements, and every fact against a reference you obtain yourself

The schema is JSON Schema 2020-12. It uses a small set of keywords (type, properties, required, additionalProperties, patternProperties, propertyNames, items, minItems, maxItems, enum, const, pattern, minLength, maxLength, minimum, maximum, anyOf, oneOf, local $ref to $defs), so any conforming validator works. Patterns are ECMAScript regular expressions with the u flag; they use only common syntax. maxLength and minLength count Unicode code points.

Canonical JSON#

evidenceDigest is the SHA-256 of a canonical encoding. Every byte counts, so follow ECMAScript's JSON.stringify exactly:

  • null, true, false as in JSON.
  • Numbers: finite only. Write them as ECMAScript's Number-to-String does. The schema allows only integers where it types a number, so str(integer) is enough there; -0 is written 0. Open objects (meta, go, cargo, pcrs) may carry any JSON; if yours carry non-integers, implement ECMAScript's algorithm (for example 1e+21, 1e-7, 0.1).
  • Strings: " and \ escaped with a backslash; \b, \f, \n, \r, \t for those controls; other code points below U+0020 as \u00XX with lowercase hex; lone surrogates as \uXXXX with lowercase hex. Everything else is written as is, in UTF-8: no \/, no escaping of U+2028, U+2029 or non-ASCII.
  • Arrays: [, items joined by ,, ].
  • Objects: {, members joined by ,, }. Each member is "key":value with the key encoded as a string. Keys are sorted by UTF-16 code units, which is not the same as code point order for characters above U+FFFF: U+1F600 (surrogates D83D DE00) sorts before U+E000.
  • A member whose value is absent is left out. There is no undefined in JSON, but mind languages where a missing optional value becomes null: null is a value and is encoded.
  • No whitespace anywhere.

Hash the UTF-8 bytes of the result.

Digest#

  1. Take the evidence object.
  2. Remove the members evidenceDigest, tpm, ima and confidential.
  3. Canonicalize the rest.
  4. evidenceDigest is the lowercase hex SHA-256 of those bytes.

The removed members are added after the digest is computed: hardware reports sign the digest, and the IMAIntegrity Measurement Architecture: the Linux kernel hashes files as they are used, logs each hash and extends PCR 10. log is authenticated by replay.

Hardware binding#

nonce and evidenceDigest are hex strings. Decode both to bytes and concatenate them, nonce first.

StatementValue
TPMTrusted Platform Module: a chip with keys that cannot be exported and registers (PCRs) that can be extended but never set. quote qualifying dataSHA-256(nonce ‖ digest), 32 bytes; tpm2-tools takes it as hex (tpm2_quote -q)
SEV-SNPAMD Secure Encrypted Virtualization with Secure Nested Paging: confidential VMs whose launch measurement the AMD chip signs. and TDXIntel Trust Domain Extensions: confidential VMs whose measurement is signed through Intel's quoting enclave. report dataSHA-512(nonce ‖ digest), 64 bytes, written to configfs-tsm's inblob

A reference in Python#

The functions below reproduce the test vectors. They restrict numbers to integers, as the evidence schema does.

Python
import hashlib
import json


def canonicalize(value):
    if value is None or isinstance(value, bool):
        return json.dumps(value)
    if isinstance(value, int):
        return str(value)
    if isinstance(value, float):
        # Evidence holds integers only; other numbers need ECMAScript's
        # Number-to-String algorithm, which Python's repr does not follow.
        if value.is_integer() and abs(value) < 1e21:
            return str(int(value))
        raise TypeError('non-integer numbers are not supported here')
    if isinstance(value, str):
        return json.dumps(value, ensure_ascii=False)
    if isinstance(value, list):
        return '[' + ','.join(canonicalize(item) for item in value) + ']'
    if isinstance(value, dict):
        # Sort by UTF-16 code units, as ECMAScript does, not by code points.
        keys = sorted(value, key=lambda key: key.encode('utf-16-be'))
        return '{' + ','.join(json.dumps(key, ensure_ascii=False) + ':' + canonicalize(value[key]) for key in keys) + '}'
    raise TypeError(f'cannot canonicalize {type(value).__name__}')


def evidence_digest(evidence):
    rest = {key: item for key, item in evidence.items() if key not in ('evidenceDigest', 'tpm', 'ima', 'confidential')}
    return hashlib.sha256(canonicalize(rest).encode('utf-8')).hexdigest()


def qualifying_data(nonce, digest):
    return hashlib.sha256(bytes.fromhex(nonce) + bytes.fromhex(digest)).hexdigest()


def report_data(nonce, digest):
    return hashlib.sha512(bytes.fromhex(nonce) + bytes.fromhex(digest)).digest()

Python's json.dumps(..., ensure_ascii=False) escapes strings the way ECMAScript does, except for lone surrogates, which Python cannot encode to UTF-8. Evidence from a real attester does not contain them.

Test vectors#

The first two vectors come from Attestium's tests (test/core.test.js). The others were computed with lib/util.js and lib/evidence.js, and reproduced with the Python functions above. Inputs are given as ECMAScript literals; hashes are over the UTF-8 bytes of the canonical output.

InputCanonical outputSHA-256
{b: 1, a: [true, null, 'x'], c: undefined}{"a":[true,null,"x"],"b":1}54a65415ad370228851a1da4b31b6fd42dc58b19a50d35cae759325f7388ce64
{b: 2, a: 1}{"a":1,"b":2}43258cff783fe7036d8a43033f830adfc60ec037382473548ac742b888292777
{z: {y: [], x: {}}, a: [{c: 1, b: 2}]}{"a":[{"b":2,"c":1}],"z":{"x":{},"y":[]}}437894c054553a7d0fd239f3e80916b06a11ae5996a90f880bfb6c109accf380
['\u00e9', '\u2028', '\u0007', '"\\/', '\u{1F600}', '\n\t']["é","<U+2028>","\u0007","\"\\/","<U+1F600>","\n\t"], where each <U+...> stands for that character, unescaped0805c852656cdd659e75ecf87a98651d80a44573fd90ae9806bf31b81c708722
{b: 1, a: 2, A: 3, '\uE000': 4, '\u{1F600}': 5, '': 6, aa: 7}{"":6,"A":3,"a":2,"aa":7,"b":1,"<U+1F600>":5,"<U+E000>":4}b139461352772b889ac8045cfdcc3ce655a1684709f6bc2d9384ba6ace6a0cc3

Rejected inputs (an implementation must refuse them rather than encode something): NaN, Infinity, an array containing undefined, and any value that is not null, a boolean, a number, a string, an array or a plain object (for example a date).

A minimal evidence document:

JSON
{
  "type": "attestium-evidence",
  "version": 2,
  "nonce": "00112233445566778899aabbccddeeff",
  "collectedAt": "2000-01-01T00:00:00.000Z",
  "attester": {"name": "example-attester", "version": "1.0.0"},
  "host": {"hostname": "web-1", "kernel": "6.8.0"},
  "services": [],
  "executables": [],
  "libraries": []
}
ValueResult
Canonical form{"attester":{"name":"example-attester","version":"1.0.0"},"collectedAt":"2000-01-01T00:00:00.000Z","executables":[],"host":{"hostname":"web-1","kernel":"6.8.0"},"libraries":[],"nonce":"00112233445566778899aabbccddeeff","services":[],"type":"attestium-evidence","version":2}
evidenceDigestd8240a4859b1d74332c2d2819f5268211bbf5748db25da964669b4f3ba8ddbe4
TPM qualifying data1e30e25b46b0dc405177c72d2aa209eb3eb54ccca7a8469b1bc681f5f1728232
Confidential VM report data901272da3f7f0600907065dea08214959c4d96888d23f50486ca2edb0f68249f0cae1ffff619eae601547abc4abf425287eed2a100cdef9f4da5cc7ee2f97ce4

Adding "evidenceDigest", "tpm", "ima" or "confidential" members does not change the digest. The document validates against the schema once evidenceDigest is added.

Verifier checks#

A verifier must, in this order:

  1. Validate the evidence against the schema. Use no field before this.
  2. Check that type is attestium-evidence and version is 2. Refuse other versions rather than guess.
  3. Check that nonce equals the nonce you sent, for this machine and this audit.
  4. Check that collectedAt is inside your accepted window (allow a small clock skew into the future).
  5. Recompute evidenceDigest and compare.
  6. Verify hardware statements only with keys you pinned, or certificate chains that end at vendor roots you ship. For a TPM quote: the signature, the qualifying data, 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. digest against the reported values, and your expected PCR values. When tpm or confidential says required: true and the statement is missing or has an error, fail.
  7. When an IMA log is present and PCR 10 was quoted, replay the log to the quoted value; use only the entries up to the point where it matches. Compare those measurements with your references (the commit, the packages' reference files, the build), not with the hashes the evidence reports, and report a service under whose files nothing was measured: the IMA policy then does not cover it.
  8. Obtain every reference yourself, by digest where it is content-addressed, and compare every fact: service files, installed packages, container files, and every executable, library and monitored file.
  9. Treat every check that could not complete as inconclusive, never as passing: incomplete entries of a process, truncated lists, errors, error fields, references you could not fetch.
  10. Report the evidence level with every result. Software evidence is the attester's word: whoever controls the machine can forge it (Forged answers).

Attester notes#

  • Answer only through an authenticated channel that runs nothing but the attester, such as an SSH forced command; never on a network endpoint.
  • Read the nonce as hex, 16 to 64 bytes, and refuse anything else.
  • Record collectedAt when collection starts, in ISO 8601 UTC with a Z (the schema's time format is YYYY-MM-DDTHH:MM:SS[.fraction]Z).
  • Hash running executables through /proc/<pid>/exe and mapped files through /proc/<pid>/root when the process is in another mount namespace, so the hash is of the file the process uses.
  • Report what you could not read (errors, incomplete, error) instead of leaving it out. A verifier cannot tell a missing fact from a clean one.
  • Never run or load what you inspect. Hash binaries and scan them for build information; do not execute them.
  • Compute the digest last, then ask the hardware to sign the binding values, then read the IMA log.

Edit this page on GitHub