Skip to content

Repository files navigation

enclave-runtime

Run an ordinary WebAssembly component inside an AWS Nitro Enclave, where the enclave itself serves every standard WASI call, down to the hardware clock and the security module.

Write the guest with plain std. SystemTime::now() reads the Nitro PTP hardware clock, getrandom reads the Nitro Security Module, std::fs writes to a store that is encrypted before anything reaches S3, and requests arrive over TLS terminated inside the enclave. The guest never touches a device path, a key or a certificate.

WASI has no way to reach a person or a service between requests, so the runtime adds three WIT capabilities: enclave:notify wakes the owner's phone without revealing why, enclave:streams holds a connection a service can use to speak first and attests every request the runtime sends over it, and enclave:tasks runs durable work on the enclave's clock while no client is connected.

Every party that relies on the enclave checks the same two measurements: PCR0 for the runtime image and its configuration, PCR16 for the guest. KMS checks them before releasing the storage key, a native app before sending a passkey-approved request, and a service on every message the runtime sends it.

Pre-1.0, under active correctness review. The runtime, its capabilities, the KMS recipient flow and the deployment tooling are implemented and tested, including end to end in an emulated enclave. Validation on real Nitro hardware and against live KMS and FCM is outstanding, as is storage garbage collection. This is not a production-readiness claim.

Contents

Plain WASI, enclave hardware underneath

The runtime links the standard WASI 0.2 interfaces and serves each one from inside the enclave boundary:

The guest writes WASI interface What answers it inside the enclave
SystemTime::now() wasi:clocks/wall-clock The Nitro PTP hardware clock (/dev/ptp0, disciplined by Amazon Time Sync), not the system clock the parent can set
getrandom, OsRng, key generation wasi:random/random The Nitro Security Module, read directly on every call with no DRBG in between
Hash-table seeding wasi:random/insecure-seed A fresh NSM draw for every instance
An HTTP handler wasi:http/incoming-handler TLS terminated inside the enclave, behind the passkey gate; the runtime sets the caller's tenant in x-enclave-tenant
An outbound HTTP request wasi:http/outgoing-handler Denied by default. Exact origins named in the measured image are allowed, verified against web PKI roots compiled into the image
std::fs wasi:filesystem A per-tenant directory in an encrypted, S3-backed store (how)
println!, eprintln! wasi:cli/stdout, stderr Bounded, structured log records tagged guest, optionally forwarded to CloudWatch
std::env::var wasi:cli/environment A curated environment: AWS_* and S3FS_* are never inherited, and the image's values are covered by PCR0
stdin, raw sockets wasi:cli/stdin, wasi:sockets Stdin is closed. Sockets are linked so guests instantiate, but every address is refused

wasi:clocks/monotonic-clock and wasi:random/insecure keep wasmtime's host-backed defaults. They measure durations and drive non-cryptographic generators; they never supply a timestamp or a secret.

Inbound HTTP/2 is negotiated per connection, so a guest can answer a bidirectional gRPC stream while it is still arriving (streaming, guest-grpc).

There is no enclave SDK. The example guest is std, wstd and wit-bindgen, and the same component bytes, and therefore the same PCR16, run in the QEMU development enclave and on Nitro.

One clock, one entropy source, one identity

The runtime opens each trusted device once at boot and hands that one handle to everything that needs it, the guest's WASI imports and the runtime's own subsystems alike. A timestamp the guest reads is the one the scheduler acted on, and every secret in the system comes from the same device.

One clock

An enclave has no NTP. The hypervisor sets its system clock, and the parent instance controls the hypervisor. The runtime instead reads the Nitro card's PTP hardware clock through the POSIX dynamic-clock interface, and every reader shares one adapter:

Reader What depends on it
Guest wasi:clocks/wall-clock Every SystemTime::now(), including expiry checks in guest code
Filesystem set-times with "now" File timestamps agree with the guest's clock
Task scheduler run-at deadlines and recurring intervals
FCM OAuth The service-account JWT's iat and exp. Google checks these, which makes this the one place an outside party checks the enclave's clock; skew shows up as invalid_grant
Notification queue Device enrollment times and retry backoff

A read that fails mid-run returns the last good reading and logs a warning. A few milliseconds stale is harmless; a zero timestamp means 1970 dates, expired certificates and rejected tokens. The production image sets S3FS_CLOCK_SOURCE=ptp, so a missing device stops the boot, while auto falls back to the host clock with a warning. The QEMU image uses the host clock because QEMU has no PTP device.

One entropy source

/dev/nsm is the device that signs attestation documents. The runtime draws every secret and identifier it hands out from the same device:

Consumer What it draws
Guest wasi:random/random Every byte, straight from the device (256 bytes per NSM request)
Guest wasi:random/insecure-seed A fresh seed per instance, so no two instances share a hash seed
Tenant IDs 16 bytes at registration, so the host cannot predict a tenant's directory before it exists
Authentication Registration and challenge IDs, and the 32-byte single-use tokens that admit a request

If the device fails, wasi:random stops the process rather than return predictable bytes. The clock can serve a stale reading because a stale timestamp is still a real one and a caller can notice. Predictable bytes look random to the guest, and a key built from them can never be detected downstream. The production image sets S3FS_RANDOM_SOURCE=nsm, and auto falls back to kernel entropy with an error-level log. The TLS key comes from the kernel pool through aws-lc-rs. Inside an enclave the NSM is that pool's only seed, and requiring /dev/nsm at boot proves the runtime is in an enclave.

enclave-runtime --self-check prints the chosen clock, its skew against CLOCK_REALTIME and a sanity check of the entropy source, then exits without touching storage.

One identity

Before asking for any key, the runtime uses the same NSM to extend PCR16 with the guest's hash and lock the register. The same device then signs every attestation document. Three independent parties check the resulting pair:

flowchart TB
    M["PCR0: runtime image and measured configuration<br/>PCR16: the guest component"]
    M --> K["KMS<br/>releases the storage key to this pair<br/>(enforced by the key policy)"]
    M --> C["Native app<br/>checks /auth/* before sending<br/>a passkey-approved token"]
    M --> S["Counterparty service<br/>checks every request the runtime<br/>sends on a held connection"]
Loading

PCR0 covers configuration as well as code: the egress allowlist, the guest environment, the FCM project, the WebAuthn origins and the bucket identities. An app that pins PCR0 has therefore also pinned where the guest can send data and which push project can wake its devices.

Capabilities beyond WASI

WASI has no interface for waking a person, letting a service speak first, or running something at 3 a.m. The runtime adds three small WIT packages for them. No function takes a tenant ID. The runtime binds each call to the tenant of the invocation making it, so a guest cannot reach another tenant's devices, connections or tasks.

A guest composes the worlds it needs. From the example guest:

world app {
    include enclave:tasks/background@0.1.0;   // import the queue, export run-task
    import enclave:notify/notify@0.1.0;        // wake the tenant's devices
}

Held connections compose the same way, with include enclave:streams/streaming@0.1.0. Guests vendor the canonical definitions from wit/, and scripts/wit-drift.sh fails if a copy drifts.

enclave:notify — wake a phone without saying why

register-device: func(token: string) -> result<_, string>;   // interactive only
forget-device:   func(token: string) -> result<_, string>;   // interactive only
devices:         func() -> result<u32, string>;               // a count, never the tokens
wake:            func(category: string, reference: option<string>) -> result<_, string>;

A guest cannot reach the network on its own, so the runtime holds the Firebase Cloud Messaging credential and sends on its behalf. A wake is data-only. The payload crosses the parent instance and then Google, so it has no title, no body and no notification object, and no setting can add one:

{"message": {"token": "…",
             "data": {"v": "1", "category": "task-done", "ref": "invoice-42"},
             "android": {"priority": "high", "ttl": "3600s"},
             "apns": {"headers": {"apns-push-type": "background", "apns-priority": "5"},
                      "payload": {"aps": {"content-available": 1}}}}}

On waking, the app attests the enclave and fetches the details over its pinned connection, which the parent cannot read.

How notify draws on the rest of the runtime:

  • Credentials. The runtime signs the service-account JWT itself, with iat and exp from the PTP clock. It sends over an HTTPS client that trusts only the web PKI roots compiled into the image, because the parent answers the enclave's DNS.
  • Authority. Enrolling or forgetting a device is a standing ability to reach a person, so it takes an interactive, passkey-approved call. Raising a wake from background work is the main use: a task finishes and tells its owner to come and look. A wake grants nothing; it uses an enrollment the owner already made.
  • Storage. Device tokens live in /runtime/devices in the encrypted store, outside every tenant's scope. A guest enrolls a token and can only ever get a count back.
  • Delivery is best effort and bounded. Repeat wakes for one (tenant, category) coalesce, and a full queue drops and counts instead of blocking the guest. Transient failures back off, and tokens FCM reports as UNREGISTERED are pruned. Each tenant can enroll 8 devices; enrolling a ninth evicts the oldest.

A stolen FCM credential lets someone send wake signals and nothing more. A wake carries no data, and the app's response to any wake is to fetch over the attested channel. Set S3FS_FCM_PROJECT_ID (measured) and, in production, S3FS_FCM_SERVICE_ACCOUNT_PARAMETER (an SSM parameter). See docs/NOTIFICATIONS.md.

enclave:streams — let a service talk first

stream-open:   func(id: string, origin: string) -> result<_, string>;   // durable; interactive only
stream-close:  func(id: string) -> result<_, string>;                   // interactive only
stream-send:   func(id: string, payload: list<u8>) -> result<_, string>;
stream-status: func(id: string) -> result<string, string>;

export on-message: func(id: string, message-id: string, payload: list<u8>) -> result<list<u8>, string>;

A guest instance lives for one invocation and cannot keep a socket open, so the runtime holds the connection: a server-sent-events GET for what arrives and one POST per message sent. After a network failure or a restart it reconnects, backing off from 1 s to 5 min, without involving the guest. Each incoming event runs on-message in a fresh instance scoped to the tenant, and a non-empty return value is posted back as the reply.

GET  <origin>/escrow/stream?id=<tenant-hex>-<id>    held open; each event → on-message
POST <origin>/escrow/send?id=<tenant-hex>-<id>      one message per request

Every request the runtime makes carries an NSM attestation document in x-enclave-attestation, bound to that request's exact bytes:

user_data = SHA-256("enclave-runtime/stream/v1" 0x00 ‖ kind ‖ 0x00 ‖ wire_id ‖ 0x00 ‖ SHA-256(body))
            kind = "open" for the GET, "send" for each POST

The service verifies the chain, pins PCR0 and PCR16, rejects documents more than five minutes old, and recomputes user_data from the URL's id and the body it received. Knowing a wire ID is not enough to pass as this enclave, whether by forging a message or by standing up a cosigner of one's own.

The origin must be in the image's egress allowlist, which the runtime re-checks on every reconnect, so holding a connection reaches nothing the guest could not reach itself. Limits: 8 connections per tenant, 256 KiB per message. Delivery is at least once in both directions, so deduplicate by message-id. See docs/STREAMING.md.

enclave:tasks — work while nobody is connected

enqueue: func(id: string, payload: list<u8>, run-at: u64, interval-ms: option<u64>) -> result<_, string>;
status:  func(id: string) -> result<string, string>;
cancel:  func(id: string) -> result<_, string>;
forget:  func(id: string) -> result<_, string>;

export run-task: func(task-id: string, payload: list<u8>) -> result<list<u8>, string>;

An approved request can schedule work for later, optionally recurring at intervals of one second or more. run-at is Unix milliseconds on the PTP clock. Task records live in the encrypted store and survive restarts. Each run gets a fresh instance, holds the tenant's lock and has a deadline. Failed runs are retried a bounded number of times, and results are kept for status. Execution is at least once, so deduplicate by the run ID (<id>:<generation>:<occurrence>). Payloads and results are capped at 64 KiB. Enable with S3FS_BACKGROUND_TASKS=true; see docs/BACKGROUND_TASKS.md.

Who may call what

The runtime marks each invocation as either interactive (a passkey-approved request) or background, and allows different calls from each:

Call Approved request run-task on-message
tasks.enqueue, cancel, forget ✓
tasks.status ✓ ✓
notify.register-device, forget-device ✓
notify.wake, devices ✓ ✓ ✓
streams.stream-open, stream-close ✓
streams.stream-send, stream-status ✓ ✓
wasi:http to allowlisted origins ✓ ✓ ✓

Background work cannot grant itself more work, connections or devices. It can only use what an approved request already set up.

Instances are disposable

A healthy HTTP instance may be reused for the same tenant, but run-task and on-message always get fresh ones, and any instance can be discarded after a trap, an eviction or a restart. Keep durable state in files and sync or close them before returning, because files a discarded instance left open are released unflushed. A tenant's requests, tasks and message callbacks run one at a time; different tenants run concurrently.

All together: a background job wakes its owner

sequenceDiagram
    autonumber
    participant App as Phone app
    participant RT as Runtime (enclave)
    participant G as Guest
    participant FCM
    App->>RT: /auth/* with a fresh nonce
    RT-->>App: attestation binding PCR0, PCR16 and the TLS certificate
    App->>RT: passkey assertion
    RT-->>App: single-use token drawn from the NSM
    App->>G: approved requests to POST /devices and POST /tasks/report
    G->>RT: notify.register-device, then tasks.enqueue with run-at
    Note over RT: later, when the PTP clock reaches run-at
    RT->>G: run-task in a fresh instance
    G->>RT: write the result, then notify.wake(task-done, report)
    RT->>FCM: data-only message, OAuth JWT signed on PTP time
    FCM-->>App: silent wake
    App->>RT: attest, approve, fetch the result
Loading

The QEMU harness runs this flow in an emulated enclave, with a stub in place of FCM that checks the wake carried no content.

Try it locally

1. Run the unit tests

From the repository root, with Rust and its native build prerequisites (a C toolchain, pkg-config and OpenSSL headers):

cargo test --workspace --lib

This needs neither Docker nor an AWS account. It covers the clock and entropy adapters, tasks, streams, notifications, authentication, attestation verification and storage. Tests marked ignored need extra fixtures and do not run by default.

2. Start the development enclave

The harness boots the real runtime image in QEMU's nitro-enclave machine with an emulated NSM. It needs Linux with KVM and vsock, Docker, Nix with flakes, Rust with the wasm32-wasip2 target, python3, jq, curl and openssl:

rustup target add wasm32-wasip2
sudo modprobe vsock_loopback

docker build -t s3fs-qemu-nitro:latest deploy/qemu-nitro   # built from source; ~680 MB
cargo install vhost-device-vsock --version 0.3.0 --locked \
  --root target/qemu-nitro/tools

# Builds the example guest and starts MinIO, a local ACME CA and an FCM stub.
deploy/qemu-nitro/dev-enclave.sh --keep-store

The first run builds an enclave image and takes a while. The harness prints the URL (default https://127.0.0.1:8443), PCR0, PCR16 and the trust-root path, then keeps running.

The emulator exercises the protocol, not AWS's hardware trust boundary. It uses a development master key, and it signs attestation documents with a chain it mints at each boot because QEMU's NSM cannot produce AWS-signed ones. It also runs on the host clock. Use it with test data only.

To build on one machine and run on another, --pack produces a self-contained bundle and --prebuilt runs one; the Publish a dev enclave workflow attaches such bundles to GitHub releases. See the development guide.

3. Make an authenticated, stateful request

In another terminal, substitute the two measurements the harness printed:

target/release/passkey-client \
  --url https://127.0.0.1:8443 \
  --state target/qemu-nitro/dev/alice.json \
  --trust-root target/qemu-nitro/dev/trust-root.der \
  --pcr0 <printed-pcr0> --pcr16 <printed-pcr16> \
  get --path /counter

The client registers a software passkey, verifies the enclave's attestation and pins its certificate, approves the request, and prints a counter that increases on each run. A different --state file registers a different tenant. The software authenticator is behind the testing feature and is not in the production image.

With --keep-store, restarting the harness preserves the store and registered credentials. Read the new pins at each boot: the emulator's attestation root changes every time, and a different guest changes PCR16.

4. Bring your own component

deploy/qemu-nitro/dev-enclave.sh \
  --guest path/to/component.wasm \
  --keep-store \
  --guest-egress http://192.168.127.254:7070 \
  --guest-env SERVICE_URL=http://192.168.127.254:7070

The guest must export wasi:http/incoming-handler, and may use the WIT capabilities. The emulator image enables background tasks by default, which requires the guest to export run-task. For an HTTP-only guest, add --guest-env S3FS_BACKGROUND_TASKS=false. 192.168.127.254 reaches the development host through gvproxy.

There is no bare laptop mode: the runtime measures its guest into PCR16 before it boots, and that needs an NSM. Host integration tests use a fake NSM; QEMU provides an emulated one.

Trust boundaries

flowchart TB
    App["Native app + passkey"] -->|HTTPS| Parent["Parent instance: gvproxy + vsock"]
    Parent -->|ciphertext only| TLS
    subgraph Enclave["Measured enclave · PCR0 runtime · PCR16 guest"]
        TLS["TLS + attested /auth"] --> Gate["Passkey gate"]
        Gate --> Guest["WASI component"]
        Sched["Task scheduler"] --> Guest
        Held["Held connections"] --> Guest
        Dev["PTP clock + NSM"] -.-> Guest
        Guest --> FS["Encrypted filesystem"]
        Guest --> Notify["Notifier"]
    end
    FS -->|opaque slabs, signed roots| S3[("S3")]
    Dev -.->|recipient attestation| KMS["KMS + SSM"]
    Held <-->|attested SSE + POST| Svc["Allowed services"]
    Notify -->|data-only wake| FCM["FCM"]
Loading

The parent provides transport and decides whether the enclave runs. With production key release and client verification configured correctly, it cannot read stored data or the enclave's TLS traffic. It can still stop the service, drop or delay traffic, observe metadata and answer DNS, which is why every outbound connection validates certificates against roots that are part of the measured image.

AWS Nitro attestation, KMS, S3's authenticated responses, Object Lock and the approved runtime and guest are part of the trust model. Attestation identifies code; it does not show the code is safe, so an approved guest is trusted with its tenant's data. Two channels out of the enclave are deliberate:

  • Guest output is untrusted text. A bounded queue drops rather than blocks, and guest records are separated from runtime events by tracing target, never by message content. Console and CloudWatch logs are operational, not audit evidence: their readers see whatever the guest printed, and the parent can write to the same CloudWatch stream.
  • Egress carries whatever the guest sends to an allowed origin. Allow only the services the guest needs.

Measured boot and key release

The image names where the guest lives, an object key in the roots bucket covered by PCR0, and the runtime measures what it actually fetched:

fetch component → SHA-256 → extend PCR16 → lock → re-read the register → KMS key release → mount

The runtime refuses to start if PCR16 was already extended or locked, or if the register after extending or locking does not hold what a client computes from the component (nitro-attest --measure and the guest-release build compute it). A guest-only update changes PCR16 and the client pins without an image rebuild; changes to runtime code or measured configuration change PCR0.

With --master-key-source kms, the runtime places an enclave recipient key in an NSM attestation request. It calls GenerateDataKey at genesis and Decrypt on resume, and accepts only CiphertextForRecipient, never a plaintext fallback. SSM holds the KMS ciphertext, and the roots bucket holds a pointer that is checked by digest. The KMS key policy is what enforces release: it must constrain both kms:GenerateDataKey and kms:Decrypt on both kms:RecipientAttestation:PCR0 and kms:RecipientAttestation:PCR16. --master-key-source static is for development and does not protect the key from its operator.

The store's own identity is checked at boot too: genesis, resume, upgrade or refusal, decided by an attested origin receipt (details).

Verifying the enclave from a client

The enclave generates its TLS key inside the boundary and never releases it. Certificates come from ACME TLS-ALPN-01 and are cached encrypted, outside every tenant's scope. That key is what lets a client check that its own connection ends inside this enclave:

Header Contract
x-enclave-nonce Required on every request: 8–64 fresh bytes, base64url without padding. Missing or malformed values are rejected before routing
x-enclave-attestation On every /auth/* response: a COSE-signed NSM document. Stripped from guest responses, so a guest cannot supply its own

The document's user_data binds the TLS certificate this connection actually served and the component:

0x12 0x20 || SHA256(TLS leaf DER) || 0x12 0x20 || SHA256(component)

A native client verifies the chain against the Nitro root, the document's freshness, its nonce, PCR0, PCR16 and the certificate hash, and pins that certificate, all before sending a token or anything sensitive. It then approves exactly one interaction:

POST /auth/request/options   {credential_id, method, path, query}   → attested challenge
POST /auth/request/verify    {challenge_id, assertion}              → single-use token (60 s)
<the request>                Authorization: Bearer <token>

The approval binds method, path and query, not the body or each message of a stream. Registration is open and creates a new, empty tenant. The runtime refuses to start if a document would exceed 16 KiB; client transports should allow at least 32 KiB for the whole response head, certificate chain included. Browser JavaScript cannot inspect the TLS peer certificate this flow depends on, so clients are native apps. See client integration and the standalone nitro-attest verifier.

Storage

wasi:filesystem is backed by a copy-on-write block store. Blocks are encrypted with AES-256-GCM, checksummed with BLAKE3 and bound to their position in the tree, which hangs off Ed25519-signed, hash-chained root records. S3 holds opaque slabs rather than one object per path, so the storage operator sees neither filenames nor plaintext. Each tenant sees its own directory as /, and the runtime, not the guest, keeps .. and symlinks from leaving it. SQLite runs unmodified in rollback-journal mode (guest-sqlite).

The store is single-writer, has no garbage collection yet, and commits through S3, so batch writes. The block tree, commit protocol, state origins, SQLite notes and embedding s3fs-core on its own are in docs/STORAGE.md; WASI/POSIX differences are in docs/COMPATIBILITY.md.

Configuration

cargo run -p enclave-runtime --bin enclave-runtime -- --help is the definitive reference. Configuration that Nix bakes into the image is measured: changing a bucket, relying party, allowed origin, FCM project, logging destination or guest environment changes PCR0. The S3FS_ prefix is left over from the filesystem's original name.

Area Main flags / environment
Trusted devices --clock-source ptp|host|auto, --ptp-device; --random-source nsm|host|auto, --nsm-device; --self-check
Component --guest-object / S3FS_GUEST_OBJECT, or --guest-path / S3FS_GUEST_PATH
Key source Required --master-key-source kms|static; --kms-key-id, --master-key-parameter, --environment
Listener and TLS --http-listen, --tls, repeatable --tls-domain, --acme-contact, --acme-directory
WebAuthn --webauthn-rp-id, --webauthn-origin, repeatable --webauthn-allowed-origin (including android:apk-key-hash:…)
Egress Repeatable --guest-egress-origin / S3FS_GUEST_EGRESS_ORIGINS
Guest environment --no-inherit-env, repeatable --guest-env NAME=VALUE or --guest-env NAME
Background tasks --background-tasks true, concurrency, timeout, total-record and per-tenant limits
Notifications S3FS_FCM_PROJECT_ID plus S3FS_FCM_SERVICE_ACCOUNT (development) or S3FS_FCM_SERVICE_ACCOUNT_PARAMETER (SSM)
Logging S3FS_GUEST_LOG_GROUP, S3FS_GUEST_LOG_STREAM; RUST_LOG (guest=warn filters guest output)
Store --bucket, --roots-bucket, --bucket-prefix, --fs-id, --region, --endpoint, --min-root-seq
Runtime limit Default
Unspent interaction token lifetime 60 s
Maximum interaction lifetime 300 s
Request progress timeout 30 s
Cached tenants / idle timeout 64 / 900 s
Requests before instance recycling 10,000
Background concurrency / attempt timeout 1 / 30 s
Background records, global / per tenant 1,024 / 64
Held connections per tenant / message size 8 / 256 KiB
Enrolled devices per tenant 8

By default the guest inherits the runtime's environment minus AWS_* and S3FS_*. Anywhere that environment is not deliberately curated, use --no-inherit-env and name each value.

Build, test, and deploy

cargo build --locked --release -p enclave-runtime
cargo build --locked --release -p nitro-attestation --features cli --bin nitro-attest
scripts/build-guest.sh http          # also grpc; sqlite after scripts/wasi-sdk.sh
cargo build --release -p enclave-runtime --features testing --bin passkey-client   # development only
Command Coverage
scripts/ci-check.sh Formatting, Clippy with warnings denied, workspace library tests, boot-origin tests
scripts/ci-guests.sh Builds the guests; tests tasks, held connections, dispatch, TLS binding, HTTP/2, gRPC, authentication and logging
scripts/ci-storage.sh Real MinIO/S3: retention, remount, corruption, rollback and competing claims (needs Docker)
scripts/ci-e2e.sh The stages above, then the full QEMU stack (needs KVM, Docker and Nix)
scripts/wit-drift.sh Vendored guest WIT matches wit/
scripts/ci-bench.sh Criterion storage and instance-cost benchmarks

cargo test --workspace alone does not replace these: ignored tests need --include-ignored, guests build separately, and some binaries need features. Keep the testing feature out of production images.

Deploying. Nix builds the measured enclave image; Packer (deploy/ami) builds the parent AMI; OpenTofu (deploy/tofu) defines buckets, retention, parent resources and logs.

nix build .#eif                                     # result/s3fs.eif, result/pcr.json
nix build .#guest-release --out-link guest-release  # guest.wasm, guest-pcr16.json
nix build .#eif --rebuild                           # rebuild and compare
  1. Set the bucket identities, filesystem ID, region, guest object key, TLS domains, relying party, egress and FCM settings in deployment.nix.
  2. Build the EIF and the guest, and keep PCR0 and PCR16 as release outputs.
  3. Provision from deploy/tofu. The KMS key, its policy and SSM access are provisioned separately for now; wire S3FS_KMS_KEY_ID and S3FS_MASTER_KEY_PARAMETER into the measured runtimeImage.env in flake.nix.
  4. Upload the approved component and bind KMS release to the approved measurements.
  5. Start the parent's enclave and gvproxy services. The parent forwards TLS; it never terminates it.
  6. From a client pinning the release measurements, verify the boot mode, the trusted devices, key release, the TLS attestation binding, the credential path and persistence.

Reproducibility, and which inputs remain pinned AWS prebuilt artifacts, are covered in the Nix build guide.

Limits and remaining work

Area Current boundary
Nitro validation Real recipient key release, refusal of substituted measurements, AWS-root attestation, PTP and NSM availability, IMDSv2 networking and CloudWatch delivery need hardware evidence
Notifications The FCM path is exercised against a stub, not yet against the real service
Delivery Tasks, held connections, notifications and logs each have their own retry and durability contract; none gives exactly-once side effects
Authorization scope A passkey approves one interaction on one route, not every payload byte or stream message. Per-message approval needs a host import that is not built yet
Single active writer No distributed writer coordination, scheduler fencing or multi-enclave failover
Garbage collection Not implemented; history and orphaned slabs accumulate
Freshness A cold mount needs an external root-sequence floor (--min-root-seq) to rule out rollback
Resource isolation Cache and queue limits exist; per-guest memory quotas and public-service admission control do not
Upgrades A new PCR16 is not a migration. The guest owns its schema and queued-payload evolution

The roadmap records milestone history and the planned garbage collector.

Repository guide

Path Contents
runtime WASI wiring (run.rs, linker.rs), clock, entropy, measured boot, key sources, TLS and auth, tenancy, tasks, streams, notify, logging
wit Capability contracts: notify, stream, tasks
crates/nitro-nsm NSM device access: entropy, PCRs, attestation requests
crates/nitro-attestation Attestation verification and the nitro-attest client
crates/s3fs-core The storage engine (docs/STORAGE.md)
examples HTTP (tasks and notify), bidirectional gRPC, and SQLite guests
deploy/qemu-nitro Development enclave, emulator self-test, end-to-end harness
deploy/nix, nix Measured configuration and reproducible EIF assembly
deploy/ami, deploy/tofu Parent AMI and AWS infrastructure
docs Client integration, notifications, streaming, background tasks, storage, compatibility, development enclave

License

Workspace packages declare Apache-2.0 in Cargo.toml.

About

A WASM enclave runtime system

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages