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.
- Plain WASI, enclave hardware underneath
- One clock, one entropy source, one identity
- Capabilities beyond WASI
- Try it locally
- Trust boundaries
- Measured boot and key release
- Verifying the enclave from a client
- Storage
- Configuration
- Build, test, and deploy
- Limits and remaining work
- Repository guide
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.
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.
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.
/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.
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"]
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.
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.
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
iatandexpfrom 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/devicesin 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 asUNREGISTEREDare 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.
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.
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.
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.
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.
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
The QEMU harness runs this flow in an emulated enclave, with a stub in place of FCM that checks the wake carried no content.
From the repository root, with Rust and its native build prerequisites (a C toolchain, pkg-config and OpenSSL headers):
cargo test --workspace --libThis 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.
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-storeThe 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.
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 /counterThe 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.
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:7070The 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.
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"]
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.
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).
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.
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.
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.
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- Set the bucket identities, filesystem ID, region, guest object key, TLS domains, relying party, egress and FCM settings in
deployment.nix. - Build the EIF and the guest, and keep PCR0 and PCR16 as release outputs.
- Provision from
deploy/tofu. The KMS key, its policy and SSM access are provisioned separately for now; wireS3FS_KMS_KEY_IDandS3FS_MASTER_KEY_PARAMETERinto the measuredruntimeImage.envinflake.nix. - Upload the approved component and bind KMS release to the approved measurements.
- Start the parent's enclave and gvproxy services. The parent forwards TLS; it never terminates it.
- 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.
| 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.
| 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 |
Workspace packages declare Apache-2.0 in Cargo.toml.