CASA self-assessment

VersionDateAuthorStatus
0.12026-08-19Daniel RothmannDraft
0.22026-08-22Daniel RothmannDraft
1.02026-08-24Daniel RothmannFinal
1.12026-09-02Daniel RothmannFinal. System context moved to the shared overview

Introduction

Eigin is a local-first personal AI app. The agent, its memory, and its tool calls execute on the user's device. The App can run an AI model on-device, or send conversations to a hosted inference service.

This document is our self-assessment against the CASA Specification v2.1.1. It describes our system, what is in scope, and makes a claim for each CASA requirement.

The assessment has been independently validated by TAC Security, an authorised CASA lab, which issued our letter of validation.

System context

See the shared system context for components and dependencies.

Scope

CASA assesses web apps and web-accessible APIs. Eigin doesn't publish a web app, and the Eigin Relay is the only web-accessible API we operate, so it is the target application.

Source is at src/eigin_relay/. Its deployment is declared as Terraform at infra/scaleway/. The claims below are based on the code that produces the running service.

The App is a mobile client, so it falls under the mobile specification rather than this one. It appears in claims primarily for the OAuth authorization it performs.

ComponentIn scopeWhy
Eigin RelayYesThe target application, including every route it serves
Relay databaseYesFirst-party infrastructure used by the target application. Stores AI credit tokens
App OAuth flowRequirements 3.2.xThe authorization the App performs (PKCEOAuthFlow.swift)
Hosted inference providersSections 1 to 3Called by the Relay. Receive conversation content
Web search providerSections 1 to 3Used by the Relay. Queries are agent-composed and gated by user approval, with the query disclosed
Connected account providersNoCalled by the App, not by the Relay. The authorization Eigin performs against them is assessed under 3.2.x
Apple App StoreNoThe Relay doesn't call Apple APIs. Its refund notifications arrive inbound on a Relay webhook, covered in Annex B

Requirements

The Relay supports a local-first app. It provides capabilities an agent cannot perform locally such as those needing a server-side key or a stable endpoint.

It is built on a privacy-preserving design, without direct user identity or retention of user data. Prompts and responses remain unread and unlogged (Annex A).

The main auth mechanism is a credit wallet, backed by a cryptographically signed purchase and addressed by a bearer token that authorizes spend without identifying the person. See Eigin Relay.

Sections 1 and 2 are therefore largely not applicable.

1. Authentication

ReqVerdictEvidence
1.1.1 – 1.1.3N/ANo user authentication service, proprietary or external, and no passwords. The only credential is a bearer credit token, which authorizes spend rather than identifying a user; guessing is infeasible (256-bit CSPRNG, section 2) and all routes are rate limited (3.1.5)
1.2.1MetNo accounts or default credentials exist, so none are present on any exposed interface
1.3.1 – 1.3.4N/ANo out-of-band verifiers; no email, SMS, or reset flows

2. Session management

The only credential is a per-wallet credit token from POST /v1/credits/redeem: 256 bits of CSPRNG entropy, looked up by SHA-256 digest, encrypted at rest with ChaCha20-Poly1305 (token.rs, bearer.rs).

ReqVerdictEvidence
2.1.1MetCredentials travel in headers only. No route accepts a token as a query parameter
2.2.1N/ANo login or logout exists. A wallet can be revoked server-side, and its token is refused from the next request
2.2.2N/ANo passwords
2.2.3N/ATokens are stateful and revocable at any time
2.3.1, 2.3.2N/ANo cookies are set or read
2.3.3MetAuthorization uses per-wallet revocable tokens, 256 bits of CSPRNG generated at redemption (token.rs). The static x-eigin-key is not a credential: it filters untargeted internet traffic and, since it ships in the app binary and is extractable, is not treated as a secret (key.rs)
2.3.4N/ATokens are opaque random values carrying no claims
2.4.1MetRedemption requires a verified Apple transaction JWS (single.rs) or an Ed25519-signed grant; spend requires the bearer token on every request. No account modifications exist. Sandbox transactions verify identically, in separate capped wallets

3. Access control

There is one role: the bearer of a credit token, able to act only on its own wallet. Every decision is server-side. The OAuth flow performed by the App is assessed under 3.2.x.

ReqVerdictEvidence
3.1.1MetEvery gated route requires the bearer token, enforced server-side before any handler runs (router.rs). The database is reached through a project-scoped IAM service account with a single permission set (database.tf)
3.1.2MetBalance, status, and pricing derive from the ledger and server-side config. No client-supplied value affects authorization
3.1.3MetAccess checks fail closed: missing, unparseable, or unknown input returns 401 or 500, never a pass (error.rs). The Apple webhook acknowledges invalid payloads with 200 but discards them unprocessed (Annex B)
3.1.4MetNo route takes a path parameter or object ID, and no query parameter names an object (api.yaml). Wallets are addressed solely by the bearer's own token hash
3.1.5MetNo cookies or browser session, so no CSRF surface. A per-IP sliding window covers every gated route and /docs by default (rate_limit.rs, router.rs), with the client IP derived via a trusted-hop count (client_ip.rs). Inference adds a per-token window and search a rolling quota (quota/). /health is excluded for Scaleway's probe; the Apple webhook keeps its own retry-cadence limiter. Deployed limits: container.tf
3.1.6MetEvery route is explicitly declared. No filesystem directory is served
3.2.1MetAuthorization Code with PKCE (S256) in ASWebAuthenticationSession (PKCEOAuthFlow.swift). No Implicit, no ROPC
3.2.2Metstate is generated per authorization and validated before code exchange; the callback scheme derives from the configured redirect_uri, so the two cannot drift (PKCEOAuthFlow.swift)
3.3.1N/AThe Relay exposes no administrative interface. Infrastructure and source control are administered through their own consoles, with MFA enforced on both

4. Communications

TLS terminates at the Scaleway edge. All cryptography comes from vetted libraries.

ReqVerdictEvidence
4.1.1MetTLS enforced declaratively: http_option = "redirected" redirects plaintext rather than serving it (container.tf). Qualys SSL Labs grades both endpoints A, offering only TLS 1.3 and 1.2 with forward-secret AEAD suites (IPv4, IPv6). Responses carry HSTS and other security headers (security/)
4.1.2MetInbound certificates provisioned and rotated by Scaleway. SSL Labs reports a trusted Let's Encrypt chain with no chain issues (IPv4, IPv6). Outbound uses reqwest against the system trust store. No private CAs, and verification is never disabled
4.1.3MetSHA-256 digests; ChaCha20-Poly1305 at rest, nonce derived per token with the uniqueness argument in token.rs; ES256 against pinned Apple roots (single.rs); Ed25519 for grants. Tokens and the 32-byte key come from a CSPRNG (secrets.tf). No MD5, SHA-1, DES, or RC4. Key storage and rotation: Annex C
4.1.4MetAEAD, so decryption failures are authentication failures and there is no padding to oracle. decrypt returns an opaque error (token.rs)

5. Data validation and sanitization

The Relay accepts JSON bodies and three typed query parameters. It renders no HTML, executes no shell, reads no user-specified file, and parses no XML.

ReqVerdictEvidence
5.1.1MetParameters deserialize into typed structs via serde; duplicates resolve deterministically
5.1.2MetNo application route issues a redirect. The only redirect is the edge's plaintext-to-HTTPS upgrade (4.1.1), whose destination is fixed and carries no user input
5.1.3MetNo eval equivalent; no dynamic code loading
5.1.4MetNo template engine
5.1.5MetOutbound destinations come from deploy-time config only (config/). model selects among pre-registered clients by catalog lookup; unknown values are rejected. No user input selects a host or path
5.1.6N/ANo XML or XPath parsing
5.1.7MetJSON and SSE only. The single HTML response, /docs, is static, reflects no user input, and carries a CSP (security/)
5.1.8MetAll queries are sqlx prepared statements with bound parameters (postgres.rs)
5.1.9MetNo process spawning
5.1.10MetNo user input reaches a filesystem path. Config paths are environment variables read at startup
5.2.1N/ANo file upload endpoint. Bodies capped at 10 MB

6. Configuration

The Relay is a single container built from a pinned base and configured entirely through Terraform-declared environment and secrets.

ReqVerdictEvidence
6.1.1Metjust security audits both Rust lockfiles with cargo audit and scans the built image and Dockerfile with Trivy (security.rs). Relay dependencies and the Alpine base scan clean; the single suppressed advisory is justified in audit.toml
6.2.1MetRelease build from a pinned base (Dockerfile). Tracing is filtered to eigin_relay=info, and errors return fixed messages with detail kept in server logs
6.3.1MetAuthorization reads x-eigin-key and Authorization only. Origin is never consulted
6.4.1MetEvery name in the zone resolves to a Scaleway resource in our own project or to Proton for mail. No third-party or unclaimed targets, and zone transfer is refused. Record set from scw dns record list, provided on request
6.5.1MetNo credentials or payment details are logged; payment runs through Apple, so card data never reaches the Relay. Tokens persist only as SHA-256 digest and ciphertext, and the eict_ prefix makes any leaked token detectable by secret scanners
6.6.1N/ANo browser storage. Native client; secrets live in the Keychain
6.7.1MetSecrets are separated from settings, generated rather than authored, stored encrypted by Scaleway, kept out of the repository, and access is monitored. Described in Annex C

Third-party APIs

The third-party APIs the Relay calls are the hosted inference providers and the Staan search API. They are assessed against sections 1 to 3 only.

ReqVerdictEvidence
Section 1N/ANeither provider gives Eigin a user account, a password, or a recovery flow. Each is reached with a single service credential
Section 2N/ANo sessions are established. Every request carries its own credential; nothing is negotiated, stored, or reused across calls
Section 3MetProvider credentials are held server-side only, injected as secret_environment_variables (container.tf) and never sent to the App. Destinations come from deploy-time config, so no client input selects a provider (5.1.5)

Onward transfer terms for the inference providers are recorded under Onward transfer.

Annex A: User data

Integrations let the user bring their own content into a chat with their agent. Eigin supports integrations with Google, Microsoft, MCP servers, and more. Integrations follow the same implementation pattern, and Google data is used here as the primary example, covering the Google API Services User Data Policy.

Google scopes

Eigin requests two restricted scopes: gmail.readonly, to read messages the user asks the agent about, and gmail.send, to send messages the user asks it to send.

Data flow

Gmail is read and sent on the device. Gmail content can reach the Relay, because tool results are added to the conversation before it is sent for inference.

flowchart LR
    subgraph device["iOS device"]
        kc[("Keychain<br/>OAuth tokens")] --- app["Eigin App"]
        db[("Local database<br/>chats, agent memory")] --- app
    end
    subgraph eigin["Eigin infrastructure"]
        relay["Eigin Relay"]
    end
    subgraph third["Processors (EU)"]
        prov["Hosted inference provider"]
    end

    app <-->|"get/send email"| google["Google APIs"]
    app <-->|"prompt / response"| relay
    relay <-->|"forwarded unread"| prov
  • Credentials don't leave the device. OAuth tokens are stored on the iOS Keychain and only used locally.
  • The Relay doesn't read prompts. It parses model and stream, forwards the rest unread, and scans responses only for token usage (handler.rs, usage.rs).
  • Zero Data Retention. No prompt or response content is logged, stored, or traced. Only token counts persist, to settle billing.
  • No user identity exists. A Relay credit wallet is keyed by a SHA-256 hash of an In-App-Purchase.
  • On-device inference bypasses all of this. If the user chooses a local model, prompts don't leave the device.

Onward transfer

Forwarding Gmail content to an inference provider is a transfer of Google user data. Our inference providers are EU-based and bound by a DPA.

ProviderLocationNo retentionNo trainingNo human review
TensorXDublin, HelsinkiDPA §3.2DPA §3.2Ephemeral enclaves
MistralFranceOrg-level ZDRFollows from ZDRSee the Trust Center

Google user data is never used for advertising or model training. See the privacy policy.

Other destinations

Those transfers are designed. Every other destination is reached through a tool the agent calls, and the agent composes its own arguments. Because a model can paraphrase around any content filter, the control is containment rather than filtering:

  • The destination set is fixed. Data leaves only through a declared tool.
  • External tools ask first. Web search, web fetch, and MCP servers are ask by default (permissions).
  • The prompt shows the arguments, so the user reviews content rather than a tool name.
  • A tool can be removed. never hides it from the agent entirely.

Web search reaches one provider, Staan. MCP servers are added by the user and gated by the same approval.

Annex B: Apple webhook

POST /webhooks/apple/appstore carries App Store refund events (webhook.rs).

Unauthenticated by design. Apple cannot present a credential, so the ES256 signature is used for authentication (4.1.3). Verification precedes any ledger access, so a forged payload reaches no wallet.

Replay is bounded by a signedDate freshness window and by notificationUUID deduplication, which admits each notification once.

200 on failure by design. Apple retries non-2xx, and a payload that will never validate should not be retried forever.

Annex C: Secrets

Separation. Non-secret settings are plain environment_variables. Secrets are secret_environment_variables, which Scaleway stores encrypted and never returns through the console or API (container.tf).

Origin. Secrets we mint are automatically generated with Terraform: 32 random bytes for the token encryption key, a 32-character random relay key, an Ed25519 grant keypair (secrets.tf). Provider API keys come from an untracked .env, since only their issuer can create them.

Database. Terraform creates a dedicated IAM application. Its API key is scoped to that database alone, so no credential is shared.

State. Generated secrets live in Terraform state, in a private, versioned Object Storage bucket in a dedicated project, reached only by a project-scoped IAM key, and configured through an untracked backend.tf (see the remote state runbook). One set per workspace, so staging and production never share a value.

Nothing committed. .env, backend.tf, and local signing keys are all ignored. The registry is private.

Monitoring. Scaleway Audit Trail records IAM and database events for the organization, always on. Injected secrets are write-only — the console and API never return them — so the monitored surface is the IAM credentials able to replace them.

Rotation. Every secret can be rotated. The relay key accepts a previous value alongside the current one, so shipped clients keep working during a change. The grant keypair and the token encryption key are replaced in place.