Documentation
Guides and reference for OutLayer verifiable compute and agent custody.
Docs navigation
Secrets
Enterprise-Grade Security with CKD & MPC Network
Secrets are protected using Confidential Key Derivation (CKD) - a cutting-edge primitive that leverages the NEAR MPC Network to provide deterministic secrets for TEE applications. Each app gets cryptographically isolated keys that persist across TEE restarts, derived through distributed computation where no single node knows the final secret.
What are Secrets?#
Secrets are encrypted API keys, tokens, or sensitive data stored on-chain. They are automatically decrypted and injected as environment variables when your WASM code executes. The keystore service running in TEE handles all encryption/decryption operations.
Creating Secrets#
Use the Secrets page to create encrypted secrets. Specify repository, branch (optional), and profile name. Secrets are encrypted client-side before being stored on-chain.
Two Ways to Create Secrets:
1. Manual Secrets
Provide key-value pairs directly (e.g., API keys you already have)
- Encrypted in your browser with ECIES (X25519 ECDH + HKDF-SHA256 + ChaCha20-Poly1305) — only the TEE can decrypt
- Example:
{"OPENAI_KEY": "sk-..."} - Cannot use
PROTECTED_*prefix (reserved for auto-generated)
2. Auto-Generated Secrets
Generate cryptographically secure secrets in TEE without seeing their values
- Generated inside TEE (nobody ever sees the value)
- Perfect for derivation keys, signing keys, encryption keys
- Must start with
PROTECTED_*prefix (proves TEE generation) - Example:
PROTECTED_MASTER_KEY - Types: hex32/64, ED25519, password:N
Naming Convention for Trust
The PROTECTED_* prefix proves a secret was generated in TEE and never seen by anyone (including developers). Manual secrets cannot use this prefix - enforced by keystore validation.
Security scope: The PROTECTED_ prefix guarantees the secret was generated inside TEE and the developer never saw its value directly. A PROTECTED_ secret cannot be accidentally leaked — it is never stored on the developer's machine and cannot be exposed through a compromised workstation or accidentally committed to a repository. At runtime, the WASM code does have access to the decrypted value (as an environment variable), so a malicious developer could potentially exfiltrate it through a backdoor in their code. For sensitive use cases, always audit the project's source code to ensure it handles PROTECTED_ secrets responsibly.
Secrets Binding Types#
Secrets can be bound to different identifiers depending on your use case:
Repository-based (GitHub)
Bind secrets to a GitHub repository and optional branch
- Key:
repo + branch + profile + owner - Example:
github.com/user/repo:main:production - Best for: Development, CI/CD workflows, version-specific secrets
- Wildcard: Leave branch empty for secrets not tied to a specific branch
WASM Hash-based
Bind secrets to a specific compiled WASM binary (SHA256 hash)
- Key:
wasm_hash + profile + owner - Example:
cbf80ed0...2f8:production - Best for: pre-compiled WASM run by URL (CodeSource::WasmUrl), immutable deployments
- Guarantees: only this exact binary can access the secrets
- A project or repository run does not read this binding. To lock their secret to one build, add the One build only access rule below
Project-based
Bind secrets to a Project - accessible by all versions
- Key:
project_id + profile + owner - Example:
alice.near/my-app:production - Best for: Long-running projects with multiple versions
- Benefit: Secrets persist across version updates - no re-creation needed
Project Binding Recommendation
For most use cases, Project binding is recommended. It allows you to update your WASM code without re-creating secrets. Create a project in the Projects dashboard, then bind your secrets to that project.
WASM Hash Binding Security
The binding is the encryption seed, not a lookup rule: the keystore seals the secret to a seed that names the hash, so a different binary does not fail a check — it decrypts nothing. A project or repository secret is protected differently: the One build only access rule is judged by the keystore against the hash the worker measured, which is why the lock can move to a new build without the value being re-entered. Neither keeps the secret inside the build: it reaches your code as an environment variable, and code that prints or sends it has leaked it.
Locking a secret to one build#
Every rule above answers who may read a secret. This one answers what may read it, and it exists because those are different questions.
When you hand a credential to a project, you are trusting its code — and that code can change under you. A new version is published and your key goes to it, without you being asked. The author need not be hostile for this to matter: a dependency moves, a build script changes, somebody force-pushes. One build only removes that trust from the arrangement. The secret opens for one exact binary, identified by the SHA-256 of the bytes that run, and a rebuild is refused. The check is made inside the attested enclave that holds the key, against the measurement the attested worker reports for the code it is about to execute — so nothing the calling code says about itself enters into it.
Your key does not become unusable at the next release. The lock and the value are stored apart: Access moves the lock to a new build without re-entering anything, so a PROTECTED_ key generated in the enclave keeps its value across every release you approve. The decision moves from the author's publish button to yours.
How to use it
- Run the project once, or open any past execution.
- Copy Executed binary from its details — that is the hash of the code that actually ran.
- Use Lock a secret to this build there, or add the One build only rule by hand.
- After each release you approve, press Access and point the lock at the new build.
Locking before the first run
You do not have to run a project to learn its hash. The platform compiles in a fixed container, so the same commit gives the same bytes and the number can be worked out in advance — then the very first run is already allowed to read the secret, instead of being the thing that tells you what to type. The order is publish, lock, run: a secret stored against a project can only be stored once that project exists, and what must not come first is a run.
Two things decide the bytes besides your source, and both have to match the platform: the compiler image and the architecture it runs on. A plain cargo build on your own machine gives a different, equally valid binary — and a hash the platform will never produce.
Who may call
A condition is judged against the account that signed the transaction. That is what lets a DAO or a router call on your behalf — but it also means a contract you sign any transaction to can relay a call that names your secret into the project it is bound to, under your own name, with input it chose. The row admits it, because the signer is you.
Direct calls only (on a secret’s Access panel, or outlayer secrets set --direct) adds a rule judged on the account that called the contract instead: a call is admitted only when that account is one the row names — you and your grantees — with no other contract in between. Name the contracts you do compose through under “Also through these contracts” (--via). In the full condition it is Predecessor wrapping any rule — a whitelist, a DAO membership, a pattern — judged on the calling account.
Two edges. Over HTTPS nothing relays a call: your payment key’s owner is judged as the calling account, so the rule changes nothing there. And a function-call access key on your own account signs directly — the calling account is you — so a dapp holding one is not something this rule can see.
The hash does not drift on its own. A project built from GitHub compiles to the same bytes every time it is built from the same commit, so a lock you set stays good until someone publishes new code — which is exactly the event it exists to catch. For that to hold, a project built from source has to commit its Cargo.lock: without one its dependency versions are chosen afresh at build time, and the bytes can change with no change to your code.
What it does not do
It decides which build may read the secret, not what that build does with it afterwards. The value reaches the code as an environment variable, so code you have locked to is code you have chosen to trust with it — review the build you pin. And for a project built from GitHub, take the hash from Executed binary rather than from a local build: the bytes depend on the compiler as well as on the source, and a build with your own toolchain gives a different, equally valid binary.
Lending a secret to an agent#
A secret you store stays yours. Handing it to an agent does not copy it anywhere: the row keeps your account as its owner and your profile as its name, and the agent merely names it in a call through secrets_ref. Take the grant away and it has nothing — there was never a second copy to take back.
A grant names any NEAR account: a person, an ordinary named account, or a custody wallet. A wallet's account is the implicit one that pays for its calls, which is why it looks like 64 hex characters; the Access form offers your own wallets by name so you need not hunt for it.
Worked example: a Gmail key, to one agent, for a day
- Store the key under your own account — Project binding, profile
gmail. The default condition admits you alone. - Press Access on the card, paste the agent's account, tick Until and pick tomorrow.
- Save. The stored condition becomes
Or[Whitelist[you], And[Whitelist[agent], ValidUntil(tomorrow)]]— you keep access, the agent's lapses on its own.
Nothing is re-encrypted at any step: Access changes the condition and leaves the value alone, so a PROTECTED_ key generated in the enclave survives every grant and revocation.
An expiry is not a revocation. The expiry is for a grant you already know the end of — a lease, a trial, a day of work — and it lapses without you doing anything. Revoking is for changing your mind sooner: remove the account and the next call is refused. The secrets page lists every account your secrets are handed to, with a one-click revoke, so a grant that outlived its agent is found rather than remembered.
The rules compose, and this is the tightest useful shape: And[Whitelist[agent], ValidUntil(tomorrow), WasmHash(build)] — one agent, one day, one build. The agent is admitted only while all three hold.
Access Control#
Control who can decrypt your secrets using flexible access conditions:
- AllowAll: anyone who names the secret can run the project with it — right for an app's own credential named in its manifest, not for a personal one
- Whitelist: specific NEAR accounts only — the default for a new personal secret under a project is your own account
- NEAR Balance: accounts with a minimum NEAR balance
- FT/NFT Balance: token holders only
- Account Pattern: regex over the account id, anchored to the whole id
- Until a date: admits only before an instant (UTC); with a whitelist under AND it is a grant that lapses on its own
- One build only: admits only a run of one exact build — the SHA-256 of the WebAssembly bytes, shown as Executed binary in an execution's details. With a whitelist under AND it is your secret that a rebuild cannot open; Access moves it to the next build without re-storing, so a generated
PROTECTED_key keeps its value across the releases you approve - Logic: AND / OR / NOT combinations of the above
Access on a secret card changes the condition without touching the value: grant an agent by its wallet account (the 64-character account that pays for its calls — your own custody wallets are offered by name), with an expiry for a leased agent, and revoke by removing it. The secrets page also lists every account your secrets are handed to, with a one-click revoke, so a grant that has outlived its agent is found rather than remembered. The agent names the secret in its call with secrets_ref.
Secrets left FOR an agent#
Everything above is a secret you own. This is the other direction: a credential stored under an agent's account, so a connector it calls can use your API token without the agent ever holding it.
The agent cannot store it itself. Its custody wallet has no NEAR to pay for the write, and the key that authorises one never leaves the TEE — so the coordinator prepares the call and a person sends and pays for it. The value is encrypted in the browser before it goes anywhere: neither the page nor the coordinator sees it.
Store one on the Secrets page, in the “for an agent” form. A link may propose which secret to create and deliberately cannot carry its value or the agent's key — both would end up in browser history, referrers and proxy logs:
https://app.outlayer.ai/secrets?project=connectors.outlayer.near/near-email&name=SENDGRID_KEYIt is filed against a scope — a project id or a WASM hash — and read only by code running under it. Cost is ~0.1 NEAR, the excess refunded.
Asking for it at call time
Nothing is fetched unless the call asks. Most calls need no secret, and a lookup that always ran would add a keystore round trip to every one of them:
curl -s -X POST -H "Content-Type: application/json" \
-H "X-Payment-Key: $PAYMENT_KEY" \
-H "x-use-owner-secret: true" \
-d '{"input":{"operation":"send"}}' \
"https://api.outlayer.ai/call/connectors.outlayer.near/near-email"The address it is read from is the agent's own account — the primary key of its payment key's row, not anything the caller can choose. That is what makes it unforgeable: only the agent's wk_ can make that wallet sign.
Using Secrets in Code#
Access secrets in your WASM code using standard environment variable functions. In Rust:std::env::var("API_KEY")
Storage Costs#
Secrets storage costs are proportional to data size plus indexing overhead (~64 bytes). Storage fees are refunded when you delete secrets.
Security Model#
Secrets are encrypted in your browser using ECIES: an ephemeral X25519 ECDH exchange against the keystore's public key, HKDF-SHA256 to derive the symmetric key, and ChaCha20-Poly1305 AEAD for the payload. The matching private key exists only inside the TEE, so not even the browser that encrypted a secret can decrypt it again — the public key published by the keystore can encrypt and nothing else.
Decryption happens in TEE workers with attestation verification, so plaintext exists only inside the enclave. Ciphertext is stored on-chain and is useless without the enclave-held key. Each encryption uses a fresh ephemeral keypair and nonce, so encrypting the same value twice produces different ciphertext, and the AEAD tag makes tampering detectable rather than silently decrypting to garbage.
Confidential Key Derivation (CKD)#
How Keystore Gets Its Derivation Key via MPC
The keystore worker itself is a TEE application that obtains its derivation key through NEAR MPC Network via DAO governance.Critically, the keystore uses a functional key (not a full access key) that can ONLY call the MPC signer through the DAO contract's request_key method. This architectural decision ensures the keystore cannot directly access the MPC network - it must go through DAO governance, making all operations auditable on-chain. Once authorized by the DAO, the keystore requests a deterministic derivation key from MPC nodes using Confidential Key Derivation. This derivation key is then used to decrypt secrets for other applications, ensuring all cryptographic operations stay within the TEE.
Phase 1: Registration (One-time)
┌──────────┐
│ Keystore │
│ TEE │
│ │
│• Generate│
│ keypair │
│• TDX │
│ measures │
└────┬─────┘
│
│ 1. Submit
│ attestation
▼
┌──────────┐
│ DAO │
│ Contract │
│ │
│• Verify │
│ attest. │
│• Create │
│ proposal │
└────┬─────┘
│
│ 2. Send
│ proposal
▼
┌──────────┐
│ DAO │
│ Members │
│ │
│• Review │
│ measures │
│• Vote │
│ (>50%) │
└────┬─────┘
│
│ 3. Approve
▼
┌──────────┐
│ │
│APPROVED │
│ │
│Functional│
│key added │
│ to DAO │
└──────────┘Phase 2: CKD Flow (Repeatable)
┌──────────┐
│ Keystore │
│ TEE │
│ │
│ Has │
│ func key│
│• Needs │
│ CKD │
└────┬─────┘
│
│ 1. Request
│ CKD with
│ func key
▼
┌──────────┐
│ DAO │
│ Contract │
│(Gateway) │
│ │
│Only func │
│key can │
│call │
└────┬─────┘
│
│ 2. Forward
│ request_key()
▼
┌──────────┐
│ MPC │
│ Contract │
│ │
│v1.signer-│
│ prod │
│ │
│Coordinate│
│derivation│
└────┬─────┘
│
│ 3. Distribute
│ to nodes
▼
┌──────────┐
│ MPC │
│ Nodes │
│ │
│ ● ● ● │
│ ● ● │
│ ● ● ● │
│ │
│ Compute │
│BLS12-381 │
└────┬─────┘
│
│ 4. Return
│ encrypted
│ derivation key
▼
┌──────────┐
│ Keystore │
│ receives │
│encrypted │
│ key │
│ │
│ Only TEE │
│ can │
│ decrypt │
└──────────┘Key Properties
Two-Level Architecture:
- Level 1: Keystore obtains derivation key from NEAR MPC via CKD protocol through DAO contract. This happens once at keystore startup, not on every secret decryption request
- Level 2: Keystore uses the cached derivation key to decrypt app secrets on demand
- All operations happen inside TEE - keys never leave the enclave
- DAO governance ensures only legitimate keystores get derivation keys
- Functional keys restrict keystore operations through DAO contract
- All key derivation requests are logged on-chain for auditability
- MPC Network ensures no single entity controls the derivation key generation
Key Properties
- • Deterministic: Same app_id always gets same secret
- • Private: Secret known only to TEE app
- • Distributed: No single MPC node has the secret
- • Persistent: Works across TEE restarts
Security Guarantees
- • BLS signatures on BLS12-381 curves
- • ElGamal encryption for transport
- • TEE attestation verification
- • Threshold cryptography (t-of-n)
Why MPC-based CKD is Revolutionary
Traditional approaches either store keys (security risk) or lose them on restart (no persistence). NEAR's MPC-based CKD is unique: it provides deterministic secrets through distributed computation where no single entity ever has the complete key. This combines the benefits of persistence, security, and decentralization - a combination not available in other systems.
DAO Governance & Keystore Authorization#
DAO Controls Keystore Access to MPC
The DAO governs which keystore workers can receive derivation keys from the NEAR MPC Network. Only TEE-verified keystores that pass DAO voting can request CKD from MPC nodes. This ensures that derivation keys are only given to legitimate, attestation-verified keystores running in secure enclaves, preventing any unauthorized access to user secrets.
Keystore Authorization Flow
- On-Chain TEE Verification: Keystore submits Intel TDX attestation directly to DAO contract. The contract cryptographically verifies the Intel certificate and TEE measurements (MRTD + RTMR0-3) on-chain. This ensures submissions can only come from genuine TEE with verified binary.
- Automated Validation: DAO contract automatically rejects any submission that:
- Doesn't have valid Intel signature
- Comes from unverified measurements (MRTD + RTMR0-3)
- Attempts to bypass TEE requirements
- DAO Voting: Only after passing on-chain TEE verification, DAO members vote to authorize keystore based on operator reputation, stake, and network capacity needs
- MPC Key Request: Once approved, keystore requests derivation key from MPC Network using CKD protocol with its unique keystore_id
- Derivation Key Receipt: Keystore receives encrypted derivation key, decrypts it in TEE, and can now decrypt user secrets while keeping all keys inside the enclave
Cryptographic Properties
The CKD protocol ensures strong security through:
- BLS signatures on pairing-friendly BLS12-381 curves
- Threshold cryptography - requires t-of-n nodes to cooperate
- ElGamal encryption for secure transport
- HKDF for key derivation from BLS signatures
This combination ensures that secrets are deterministic yet unpredictable, persistent yet secure, distributed yet accessible only to authorized TEE apps.
Security Properties
- • No single point of failure: Distributed MPC nodes
- • Forward secrecy: Fresh key pair for each request
- • TEE isolation: Secrets computed inside enclave
- • Threshold security: Requires multiple nodes
Trust Model
- • Intel TDX attestation verification
- • MPC network consensus
- • Smart contract enforcement
- • Cryptographic correctness proofs
Example: CKD Request
How a TEE app requests a deterministic secret:
// TEE app generates key pair let (a, A) = generate_elgamal_keypair(); // Include A in attestation report_data let attestation = get_tdx_attestation(A); // Call developer contract developer_contract.get_key(attestation, A); // Developer contract validates and calls MPC mpc_contract.gen_app_private_key(A); // Receive encrypted secret (Y, C) // Decrypt: sig = C - a·Y // Derive: secret = HKDF(sig)
The final secret is deterministic for app_id but known only to the TEE app.
CKD & MPC FAQ#
What happens if the keystore restarts?
The keystore can request the same derivation key again from NEAR MPC using its keystore_id. Since CKD is deterministic, it will receive the same derivation key. This allows the keystore to continue decrypting user secrets after restarts without storing keys on disk.
Can MPC nodes or DAO see my secrets?
No. MPC nodes only generate the derivation key for the keystore when requested by the DAO contract - they never see user secrets. Importantly, MPC Network only responds to requests that come through the DAO contract transaction, not direct requests. The DAO governs which keystores can receive derivation keys but has no access to the keys themselves. User secrets are encrypted and only the keystore (running in TEE) can decrypt them. No entity outside the TEE ever has access to plaintext secrets.
How is this different from regular key storage?
Traditional systems either store keys (security risk) or generate random keys that are lost on restart. CKD provides deterministic secrets through distributed computation - persistent yet secure, distributed yet accessible, a unique combination enabled by MPC and TEE technologies.
What prevents unauthorized access to secrets?
Multiple layers: (1) DAO governance controls which keystores can receive derivation keys, (2) TEE attestation verification ensures only genuine TEE apps run the keystore, (3) MPC Network only responds to requests from DAO contract (not direct requests), (4) Threshold cryptography requires multiple MPC nodes to cooperate, (5) All cryptographic operations happen inside TEE enclave.
Why use BLS signatures on BLS12-381 curves?
BLS signatures provide unique properties: deterministic, aggregatable, and efficient verification. BLS12-381 is a pairing-friendly curve specifically designed for cryptographic protocols, offering 128-bit security with optimal performance for threshold cryptography and MPC operations.