Skip to content

Threat Model & Proof Semantics

This page states precisely what an AuditProof establishes, what it does not, and what the capsule boundary defends against. It is written for a reader who intends to break the claim. The limitations section is the longest one on purpose.

The claim, in one sentence

An AuditProof verifies the integrity and ordering of the presented record and that it was signed by the holder of Nanorix's published signing key. It does not by itself prove event completeness, collector honesty, host integrity, absence of unobserved side effects, or regulatory compliance.

Every other sentence on this page is a refinement of that one. If you find a place where the product claims more than that sentence allows, that is a bug — see Coordinated disclosure.

Deployment posture. The Nanorix-hosted service is an evaluation sandbox and a control plane that does not process regulated data. Production regulated-data processing runs in customer infrastructure (BYOC, per ADR-044), where the same capsule isolation applies on hardware the customer controls. The distinction matters for several limitations below, which are called out where they apply.


1. What an AuditProof is

An AuditProof is a signed lifecycle attestation: a structured record of what a capsule runtime asserted happened during a capsule's life, bound into a hash chain and signed.

Attach that descriptor mentally every time you read the brand noun. The cryptography protects the record. It does not, and cannot, make the events the runtime asserted true. A hash chain over a lie yields a well-formed chain over a lie. What the chain buys you is that the lie must be told once, at collection time, by a component whose behaviour is specified and open to inspection — and that it cannot be revised afterwards without detection.

Concretely, the document contains:

  • An 8-step destruction chain. Fixed step count, fixed order, fixed subsystem and method names — eee_namespace, eee_tmpfs, eee_memory, dire_keys, dire_identity, fgx_forensic, rzl_audit, capsule_destroy. Each step's hash is SHA-512(prev_hash ‖ 0x00 ‖ subsystem ‖ 0x00 ‖ "destroy" ‖ 0x00 ‖ method ‖ 0x00 ‖ timestamp), seeded from the genesis value SHA-512(""). The step names are wire format, not internal codenames; they are stable forever by specification.
  • An activity trail. Typed events recorded during the capsule's life — uploads, executions, outbound calls, and their hashes.
  • An attestation block. An Ed25519 signature, the public key, and a key identifier.

The formula above is published, and the whole document is recomputable offline from it by a verifier Nanorix does not run and cannot influence. That is the entire basis of the trust story: you are not asked to believe a Nanorix API response.

On implementation independence. Nanorix publishes an open specification, an open-source reference verifier in Rust, and a corpus of 100 pinned proof documents with committed verdicts that you can run yourself. Verifier implementations in other languages are in progress and are not currently held out as producing identical results to the reference verifier. Where this page says "the verifier", it means the Rust reference implementation.


2. What the signature covers, by version

The signed message is not the whole document, and it differs by version. Knowing exactly which bytes are covered is a prerequisite to reasoning about what tampering is detectable.

cdp_version Signed message Notes
1.0 final_hash — step 8's chain_hash, as a 128-byte ASCII hex string (not the 64 raw bytes) The chain is covered transitively; the activity trail is not bound into the signed message.
2.0 document_hash
2.1 (nanorix_only) canonical_hash = hex(SHA-512(JCS(canonical_view))) RFC 8785 canonical JSON over a 16-field view. This is the current format.

The v2.1 canonical view

The signed view contains: version, signing_mode, jurisdiction, authority_id, signing_key_version, capsule_id, org_id, activity_trail, destruction_chain, destruction_state, destruction_failure_step, hash_algorithm, signature_algorithm, an attestation subset, and — when present — parent_audit_proof_id, cdp_kind, parent_proofs_merkle_root, record_receipts_merkle_root, runtime_attestation.

Because the view is canonicalised with RFC 8785 before hashing, physical key order in the JSON is irrelevant. A key reordering is either semantically identical (and correctly still verifies) or it changed a hashed value (and the signature fails). There is no "reorder the JSON to smuggle a change" attack.

What is deliberately excluded, and why that is safe

The attestation subset inside the signed view carries only timestamp_attestation and attestation_chain_fingerprint. The attestation's signature, public_key, and key_id are excluded from the bytes being signed.

The first exclusion is unavoidable — a signature cannot cover itself. The other two are safe for reasons worth spelling out, because "the key is outside the signed data" is exactly the kind of thing a skeptical reader should flag:

  • Mutating signature. The message is unchanged, so the mutated signature simply fails to verify. Detected at stage 7.
  • Mutating key_id. The verifier does not resolve keys by key_id. It resolves the pair (authority_id, signing_key_version), and both of those are inside the signed view. A tampered key_id is a cosmetic string change that routes nothing.
  • Substituting public_key with an attacker-controlled key and re-signing the document. This is the real attack, and it is why the verifier reports two different success states. Such a document will verify against the key embedded in itself — that is stage 7, integrity, and it means only "these bytes have not changed since whoever signed them signed them." It is rejected at stage 8, authenticity, where the signing key is resolved against a separately signed trust-chain manifest that the attacker cannot mint keys into.

This is why a verification run performed without a trust-chain manifest reports stage 7 and not "verified", and why you should treat stage 7 as tamper-evidence only. The corpus category 09_tamper_patterns includes byte-flips, step reordering, version downgrade, and signature substitution as pinned fixtures.

Verification stages

The reference verifier reports the highest stage reached, so a failure tells you where the document stopped being defensible rather than just "invalid":

  1. Schema validation
  2. cdp_version recognised
  3. Chain reproducibility — recompute every step's SHA-512 from genesis
  4. Final-hash binding — final_hash equals step 8's chain_hash
  5. Canonical-hash binding (v2.x)
  6. Signing-key resolution — (authority_id, signing_key_version) → public key
  7. Ed25519 signature verification → integrity
  8. Authority status in the trust-chain manifest → authenticity

Each failure emits a typed reason (StepHashMismatch { step_idx, subsystem }, RegionMismatch { required, actual }, AuthorityRevoked, and so on) rather than a bare boolean, so a failing proof is diagnosable without access to Nanorix.


3. What the boundary defends against

Retroactive tampering with the record

Editing any canonical-bound field — a chain step, an activity event, the capsule ID, the jurisdiction — changes the canonical hash and invalidates the signature. The record is tamper-evident, which is a weaker and more accurate word than unalterable: nothing prevents an attacker from altering a copy of the JSON on disk, and Nanorix makes no availability claim over documents you hold. What the construction guarantees is that an altered copy stops verifying, and that the verification is something you run, not something you request from us.

An agent editing its own history

For agentic workloads this is the load-bearing property. The record is produced outside the agent's control plane: the activity trail is written by the runtime that hosts the process, not by the process, and the attestation signature is applied after the destruction chain completes — by which point the capsule's own key material has already been destroyed as a hashed step inside the chain being signed. An agent cannot retroactively edit a transcript it never held, and cannot suppress the record of its own key destruction without breaking the chain that records it.

This does not mean the runtime is beyond suspicion. It means the trust question is narrowed from "do I trust this agent?" to "do I trust this runtime's collection", which is a specified, inspectable component rather than a model output. See collector honesty below.

Undeclared egress

Network isolation is enforced at the network namespace, below the process — not by a library the workload could decline to call, and not by a policy the workload can rewrite. A capsule with no declared egress has no network interface at all; the failure mode for an undeclared destination is that the socket does not exist, not that a check returns false.

Where egress is declared, the allowlist is fixed at capsule-create time and cannot be widened for the lifetime of that capsule. Denials are recorded in the activity trail, so an attempt is itself evidence.

TLS pin maps are a declared-but-not-yet-enforced field: they are accepted through the API and bound into the Capsulefile content hash, but per-host fingerprint verification is not active on the production path in the current release, and capsules validate against system roots. A compromised DNS answer pointing an allowed hostname at an attacker's host is therefore not currently defeated by pinning. Treat this as an open item in your own threat model rather than a control we provide today.

These are fine-grained network controls and they are genuinely below the workload. They still constrain only what crosses the boundary — see the next section.

Forgery of a proof for a capsule that never ran

Forging requires a signing key that resolves in the trust-chain manifest. The default signing path generates a fresh Ed25519 keypair per capsule and destroys the private half at the moment of signing — it signs exactly once and is then zeroized, so there is no long-lived private key sitting anywhere to steal. Deployments that prefer a persistent, non-extractable key can supply a KMS-backed signer through the same interface; the manifest anchoring is identical either way.

Non-repudiation, stated carefully

A verifying signature establishes that the holder of a specific published key signed these exact bytes. It does not establish that a human authorised it, that the signing service was behaving correctly, or that the key holder is who a certificate says they are beyond what the manifest asserts. Treat it as key-holder attribution, which is what it is.


4. What an AuditProof does not establish

This is the section that matters. Each item is a real limitation, not a hedge.

It attests destruction, not correctness

The chain attests that the workspace teardown sequence executed. It says nothing about whether the computation inside that workspace was correct, whether your model produced a sane answer, whether your code had a logic bug, or whether the output you exported is the output you wanted. A capsule that computed a wrong result and then destroyed itself flawlessly produces a perfectly valid AuditProof.

The boundary constrains what crosses it, not what happens inside it

An allowed egress endpoint is still an exfiltration channel. If your workload is permitted to call an inference API and it embeds regulated data in the prompt, the AuditProof records that a call was made and hashes the request and response — it does not, and by design cannot, inspect the content for whether it should have been sent. Nanorix does not scan or classify customer data content; that is an invariant, not a gap. The consequence is that egress declaration is a decision you own. We record it faithfully; we do not adjudicate it.

Similarly, in-capsule side channels — timing, memory pressure, cache behaviour — are not addressed by namespace isolation. On shared infrastructure, a co-tenant adversary with the ability to observe host-level metrics is outside what this boundary defends. If your threat model includes co-tenancy, use dedicated tenancy.

"Volatile memory only" is a property of the workspace, not of the host

Inside the capsule, storage is RAM-backed tmpfs with per-capsule swap disabled at the cgroup, and memory is overwritten before teardown. That property stops at the workspace edge. On customer (BYOC) infrastructure, data can be captured outside the workspace by mechanisms Nanorix neither controls nor observes:

  • host swap or hibernation writing pages to disk
  • kernel crash dumps (kdump) or process core dumps capturing memory
  • host-level observability — eBPF agents, memory profilers, container runtime log collection
  • hypervisor-level snapshots or live migration of the node

Operators running capsules on their own infrastructure should disable swap and hibernation on those nodes, disable core-dump collection or ensure the dump target is encrypted and access-controlled, and exclude capsule cgroups from host memory-profiling agents. Nanorix cannot verify that you did any of this, and the AuditProof does not attest to it. A proof from a node with swap enabled looks exactly like a proof from a node without. If host-level assurance is part of your requirement, it has to come from your own configuration management and your own evidence, not from us.

It does not prove what ran, and it does not prove the destruction in the physical sense

The runtime_attestation field exists in the canonical view and is null in every proof issued today. Nothing binds the chain to kernel state, to a measured boot, or to a hardware root. What the signature proves is that the holder of a specific key committed to a specific sequence at a specific time and cannot silently revise it afterwards. The runtime that performs the teardown is the runtime that reports it, so the record is information produced by the entity, and an assessor's completeness and accuracy testing does not go away because the record is signed. We say this to every auditor we write to, and it has to be true on this page as well as in the letter.

The activity trail records runtime events, not model reasoning

The trail includes a ReasoningStepCompleted event carrying hashes of a prompt and a response. This is a runtime event emitted at an API boundary. It is not access to a model's chain of thought, it is not evidence of what the model "actually reasoned", and it should never be read as insight into model internals. It records that a step occurred and binds the request and response bytes to a hash. Any claim beyond that — ours or anyone's — is overreach.

Collector and completeness

An AuditProof attests to the events the runtime recorded. It cannot prove that the set of recorded events is complete. A collection bug, a runtime compromise prior to signing, or an event class not yet instrumented would each yield a valid signature over an incomplete record, and the signature would tell you nothing was wrong — because from the chain's point of view, nothing is. This is the honest limit of any attestation system that signs its own observations, and it is the reason the central sentence names collector honesty explicitly.

What narrows it: the collection path is specified and its output format is open, the chain forces the record to be fixed at destruction time rather than assembled later, and the verifier is independent. What does not narrow it: our assurances.

Key custody and the state of trust anchoring

Stated plainly, because this is where attestation systems usually overclaim:

  • The per-capsule signing key is ephemeral and destroyed at signing; the public key is published and retrievable without authentication from the key registry (GET /v1/keys/:id).
  • The trust-chain manifest binds (authority_id, signing_key_version) pairs to published keys with archive-forever discipline — a rotated-away version is never removed, so a proof signed today remains verifiable after rotation. The manifest is itself signed by a long-term identity key whose fingerprint is published out of band.
  • The production manifest is published at https://verify.nanorix.io/.well-known/trust-chain.json (also https://nanorix.io/trust-chain.json), with the identity fingerprint at https://verify.nanorix.io/.well-known/identity.txt. The offline verifier consumes it via --trust-chain with the fingerprint pinned via --identity-fingerprint; the browser verifier never fetches it for you, so obtain both through a channel you trust. A bare nanorix-verify proof.json with neither flag reports integrity and prints the two commands that reach a full anchored verdict. The manifest is signed by a long-term identity key; it is not yet hardware-rooted, and saying so is part of this page's job.
  • There is no external transparency log. Nanorix does not currently anchor proofs or the manifest to any third-party append-only log. A reader who wants split-view resistance — assurance that we could not show different manifests to different auditors — does not have it from us today. Say so in your own risk assessment; do not let us imply otherwise.
  • Customer-held signing keys are half wired — the registration half, not the signing half. You can register a signing authority today and publish its key for third-party retrieval (GET /v1/authorities/:authority_id/keys/:key_version, GET /v1/customer-authorities/:authority_id/attestation). What does not yet happen: no proof is signed with a customer key — the customer_attestation slot exists in the document and is never populated — and no verifier validates a dual-signed proof. The format accepts three signing_mode values (nanorix_only, dual_signature, tee_attested); only nanorix_only is issued, and only nanorix_only is verifiable end to end. Treat dual-signature as specified and unshipped, not as available.

A signing mode this build cannot verify is a rejection

If the verifier encounters a signing_mode it cannot check — dual_signature today, or tee_attested — it rejects the document. It reports algorithm_unsupported with the offending mode and does not proceed. It does not report success, and it does not report the partial "chain verified · signature NOT checked" verdict either.

That distinction is deliberate and worth understanding, because it is the one thing you would attack. signing_mode lives inside the canonical hash, so an attacker can change it — and a verifier that answered "chain verified, signature not checked" for an unrecognised mode would let that attacker convert a rejection into a reassuring partial result by editing one field. So two conditions that look similar are kept apart:

  • No signature present at all (an unsigned partial record) → stage 4, valid: true, "signature NOT checked". Inconclusive, not a pass.
  • A declared mode this build cannot verify → rejected outright, algorithm_unsupported.

If your pipeline consumes verifier output programmatically, still gate on the stage reached rather than the top-level boolean: a stage-4 result and a stage-8 result are very different claims wearing the same valid: true. When we do ship dual-signature, older verifiers will reject proofs issued under it and tell you to upgrade — that is the intended behaviour, not a regression.

It is a structural record, never a regulatory judgment

An AuditProof describes what happened. It never asserts that what happened is adequate under any framework. Three roles, kept separate on purpose:

Role Owns
Nanorix Supplies structural evidence — what the runtime did, cryptographically bound
The customer Owns their compliance program, their controls, and their determination of adequacy
The auditor or regulator Adjudicates whether the evidence supports the determination

The regulatory_context output is a citation, not a claim: it maps a capsule's declared context to framework references using applies_to / references / related_to, carries a disclaimer and a framework_version, and never asserts adequacy. Nanorix does not certify anyone against anything, and any Nanorix artifact you find implying otherwise is a defect worth reporting.

Note also that data_classification and jurisdiction are customer-declared. We store them and bind them into the proof; we never verify or override them. Declare general while uploading regulated data and the AuditProof will faithfully record your declaration, not the truth.


5. Trust boundaries

  ┌──────────────────────────────────────────────────────────┐
  │ Customer's compliance + ops trust domain                 │
  │                                                          │
  │  ┌──────────────────────────────────────────────────┐    │
  │  │ Independent verifier — replays the proof offline │    │
  │  │ (Nanorix does not run it and cannot influence it)│    │
  │  └──────────────────────────────────────────────────┘    │
  │                       ↑ AuditProof JSON                  │
  ├───────────────────────│──────────────────────────────────┤
  │ Nanorix's attestation domain                             │
  │  ┌──────────────────────────────────────────────────┐    │
  │  │ Per-capsule isolated workspace                   │    │
  │  │ ┌──────────────────────────────────────────────┐ │    │
  │  │ │ Customer's workload code + customer's data   │ │    │
  │  │ │ (Nanorix CANNOT inspect — only attest that   │ │    │
  │  │ │  the workspace was created and destroyed)    │ │    │
  │  │ └──────────────────────────────────────────────┘ │    │
  │  │ ┌──────────────────────────────────────────────┐ │    │
  │  │ │ Egress recorder — every outbound call bound  │ │    │
  │  │ │ into the activity trail by hash              │ │    │
  │  │ └──────────────────────────────────────────────┘ │    │
  │  └──────────────────────────────────────────────────┘    │
  │  ── host below this line is NOT attested ───────────────  │
  │     kernel, swap, crash dumps, hypervisor, node agents   │
  └──────────────────────────────────────────────────────────┘
                          ↑ egress, allowlisted (TLS pinning declared, not yet enforced)
                          ↓
  ┌──────────────────────────────────────────────────────────┐
  │ Third-party endpoints (inference APIs, payer systems, …) │
  │ — Customer's trust relationship; outside Nanorix's scope │
  └──────────────────────────────────────────────────────────┘

Nanorix attests to the middle band. The host beneath it is the operator's responsibility on BYOC. The endpoints above it are the customer's relationships — we attest to what was sent and what came back, never to what the recipient did with it.

A note on the hypervisor

Namespace isolation runs above the hypervisor. An adversary with hypervisor-level access can read guest memory directly and none of the Linux primitives below them apply. Confidential-computing substrates (AMD SEV, Intel TDX) are the real answer to that threat and are a roadmap item, not a current claim. If your threat model includes the infrastructure provider, running on your own hardware under BYOC changes who that adversary is, but does not change the structure of the argument.


6. Verify it yourself

The point of an open specification is that you do not have to take any of the above on faith.

# Verify a proof offline, integrity only (stage 7)
nanorix-verify auditproof.json

# Anchor the signing key to a trust-chain manifest (stage 8)
nanorix-verify auditproof.json \
  --trust-chain trust-chain.json \
  --identity-fingerprint <pinned fingerprint>

# Run the reference corpus: 100 pinned documents with committed verdicts
cargo test -p nanorix-verify --test corpus_sweep

The corpus is the artifact to run first if you are trying to break the claim. It pins ten categories including chain mismatch, invalid signature, canonical-hash drift, region mismatch, unknown authority, and twenty explicit tamper patterns — each with its expected verdict, stage reached, and full failure reason committed alongside it. The sweep also regenerates every fixture and diffs, so the committed bytes cannot drift away from the code that produced them.

If you can produce a document that the reference verifier accepts at stage 8 and that misrepresents what a capsule did, we want to hear about it before you publish.


Coordinated disclosure

Report verification-breaking findings to [email protected]. Machine-readable contact details are published at https://nanorix.io/.well-known/security.txt.

Useful things to include: the proof document (redact payload hashes if they are sensitive), the verifier version and command line, the stage reached, and what you expected instead.

On response times. Nanorix is a very small team, so the following are statements of intent rather than a contractual SLA, and we would rather say that than post numbers we cannot always hit: we aim to acknowledge within one business day, trade an initial assessment within a week, and agree a disclosure timeline with you rather than impose one. We will credit reporters who want credit. We will not threaten researchers acting in good faith, and we will not ask you to stay quiet indefinitely.

Findings in your own workload — a bug in your code that writes regulated data to a log, for example — are outside what we can remediate, but we can help you trace which capsule's activity trail recorded the event. Contact [email protected] for that.