Skip to main content
Every Lit Action has its own key: Lit.Actions.getLitActionPrivateKey() returns a secp256k1 private key that the TEE derives from the action’s IPFS CID. Change one byte of the source and you get a new CID and a new key. That property is usually described as a guarantee (“only this exact code can sign”). It is also a factory. Take one audited template, append a small JSON constant, and you have a brand-new immutable action with a brand-new key that only that code can use. Repeat with a different constant and you have another. We call these derived actions, and they are the primitive underneath the Lit Agent Keychain, where every stored secret is its own derived action.
Nothing is uploaded to IPFS. Chipotle computes the CID from the source you submit on every call. Enroll that CID in a group when using a scoped execution key; a wildcard execution key can run the source without a group.

Who can use an action-derived key?

An action-derived key belongs to the action’s code identity, not to the account paying for execution. Within the same key-derivation environment, identical source produces the same key across accounts and usage keys. Changing runtime js_params does not change the key; changing the source does. Lit.Actions.getLitActionPrivateKey() returns the key for the currently executing action to that action’s code. It accepts no CID argument, so code cannot request another action’s private key. Public-key and address lookups can accept another CID because they return only public information. A valid wildcard usage key (execute_in_groups: [0]) can execute an unregistered action and let it use its own derived key without creating a group or PKP. PKP wallet access is separate: an action requesting a PKP private key or PKP encryption/decryption still needs a matching action-and-wallet group in the key’s account. Put authorization for derived-key operations inside the action. Anyone with permission to execute the source, including someone using their own account’s wildcard key, can invoke its behavior. An action that signs any caller-supplied message allows those callers to request signatures. An action that embeds an owner’s identity and verifies the owner’s signed request before signing or decrypting enforces that policy for every caller. Removing the verification changes the source, CID, and derived key. Merely embedding an owner address is not enough; the code must verify authorization and bind it to the requested operation. The caller does not automatically receive the raw private key. However, code that returns or logs it exposes it, so never export the key or expose unrestricted signing/decryption when building a protected action.

Why you’d want this

The last row is the important one. Anything in the source is trusted; anything in js_params is not. Moving configuration from parameters into the source turns it from caller-controlled input into part of the action’s identity.

The pattern, step by step

1. Write the template once

The template is ordinary action code that reads its configuration from a constant instead of from js_params. Keep every dependency inside the file so the bytes are reproducible: either bundle with esbuild into a single IIFE, or use version-pinned imports whose specifier bytes never change.

2. Derive an instance by appending a canonical constant

canonical() must be deterministic: sort object keys, reject floats and undefined, and never depend on insertion order. Two parties who compute the source independently (a browser, a backend, a verifier) must get identical bytes, or they will get different CIDs. The Keychain’s implementation is protocol/crypto.ts; it sorts ASCII property names and permits only safe integers.
Changing the template’s bytes changes every instance’s key. A dependency bump, a minifier upgrade, or a reformat gives every derived action a new CID and orphans every key derived from the old one. Treat the built template as a release artifact: commit the bundle, record its hash, and introduce new templates as an explicit migration. The Keychain records sha256(template) in generated/release.json and refuses to silently rebuild a deployed template.

3. Compute the CID locally

Chipotle identifies an action by the CIDv0 of its UTF-8 source (UnixFS, 256 KiB chunks, balanced layout). You can compute it in three ways:

4. Enroll the CID in a group

Skip this step when using a wildcard execution key and no PKP wallet. For a scoped execution key, add the derived CID to a group exactly as you would any action. add_action_to_group takes the raw CID; add_group takes keccak256 hashes of the CID string.
Newly granted permissions are eventually consistent. Poll the real path (run the action with the real key) rather than sleeping a fixed amount. See the API guide.

5. Execute by sending the source

There is no upload step. Every execution submits the full derived source as code; the server hashes it and checks execution permission. Scoped usage keys require a permitted group matching that CID; wildcard usage keys do not require a group to execute it.
Because callers can compute the CID themselves, they can also verify they are talking to the action they expect: compute cidForCode(code) and compare it with the CID whose key signed the response.

Give each caller an execute-only key

Protect derived-key operations with authorization in the action itself. To limit which actions callers can execute using your account’s credentials, hand end users a usage key that can execute and nothing else:
Such a key cannot add new CIDs to the group. If the group permits only specific CIDs, the key can execute only those actions and can use only the PKPs permitted alongside them. A group with the all-actions wildcard deliberately permits arbitrary code. These restrictions apply to this usage key; they do not stop someone from executing the same derived action using their own account’s wildcard key. What it can do is pay for executions, so treat it as a billing credential, not an authority credential. In the Keychain, the per-vault execution key is deliberately given to owners and agents; authority comes from signatures the action verifies, never from the key. Read API Keys for the permission model.

Discovering an action’s public key without running it

Verifiers need the public key of a derived action before trusting anything it signed. Two options: Inside another action: Lit.Actions.getLitActionPublicKey({ ipfsId }) and getLitActionWalletAddress({ ipfsId }) work for any CID. They require no group permission (the key is public) and count toward the per-execution limit of ten key operations. From outside Lit: run a tiny fixed helper action that does nothing but look up a key. This is what the Keychain SDK does; the helper is enrolled in a group every usage key can execute.
Fetch public keys from the Lit origin you trust, directly over TLS, and cache them. Never accept a “here is the action’s public key” value from an intermediary such as your own backend, because a compromised backend could substitute a key it controls. The Keychain SDK pins the Lit origin at build time and refuses replacement endpoints from API responses. For a stronger check, verify the TEE itself with remote attestation before the first request.

Deriving an encryption key from the action key

The identity key is a 32-byte secp256k1 scalar. Run it through HKDF and you get keys for other purposes that are still bound to this exact code. The Keychain derives an X25519 key for HPKE so that a secret can be encrypted to a derived action in the browser, with no PKP at all:
The client must learn encPub safely. The derived X25519 key is not the identity key, so getLitActionPublicKey cannot return it. Instead the action returns a binding signed by its identity key, and the client verifies the signature against the identity public key obtained through discovery above:
The client checks the challenge matches the one it sent, the manifest hash matches the manifest it derived the CID from, and the signature verifies under the identity key for that CID. Now it can HPKE-seal a secret to encPub knowing only this exact code can ever open it. The full flow, including AES-GCM payload encryption with metadata as additional authenticated data, is in the Keychain’s protocol/crypto.ts and actions/secret-common.ts.

Operational rules that fall out of the pattern

  • Request the private key last. Do every independent check (signatures, windows, policy) before calling getLitActionPrivateKey. A rejected request should never have touched key material.
  • Zero buffers in finally. Call .fill(0) on key and plaintext Uint8Arrays. JavaScript strings cannot be erased, so keep secrets in byte arrays where practical.
  • Return one generic error. The Keychain actions return { ok: false, error: "access_denied" } for every failure so that callers cannot probe which check tripped.
  • Updating a secret does not rotate the action key. Encrypting a new value to the same action leaves its CID and derived key unchanged. Changing the source creates a new identity and requires an explicit migration; it does not disable the old identity or erase old ciphertext.
  • Group removal is not global retirement. Removing a CID from a group revokes that group’s grant. Other permitted groups, all-actions groups, or wildcard execution keys can still authorize execution of the same source with the same derived key. Deleting your usage key likewise does not prevent another account from executing the source. If you need revocation, design the action to verify current authorization or revocation state from an authenticated source before sensitive operations. Removing a group entry alone cannot retire a derived key.
  • Version the constant. Include a protocol version in the manifest so a future template can recognize and migrate old instances.
  • Keep the fetch budget in mind. Actions may make up to fifty outbound requests, and responses are capped at 1 MB. See Limits.

Where the Keychain uses this

A user’s whole vault is therefore N+1 immutable actions built from three templates, and anyone can regenerate every CID from the public manifests and confirm the exact code that guards each secret.

See also