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, so the “publish” step is just computing a hash locally and enrolling it in a group.

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

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 for how, and Read-after-write staleness for why (the per-instance authorization cache and its ~10s catch-up window).

5. Execute by sending the source

There is no upload step. Every execution submits the full derived source as code; the server hashes it, checks the usage key’s group permissions against that CID, and runs 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

A derived action is only as safe as the key that can run it. Hand end users a usage key that can execute and nothing else:
Such a key cannot add a new CID to the group, so it cannot get any other code run against the group’s PKPs. It cannot change the derived action’s code, because that would change the CID and fall outside the group. 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.
  • Rotate by re-encrypting, not by re-deriving. To rotate a secret held by a derived action, encrypt the new value to the same action; the CID and key stay put. To retire an action, remove its CID from the group.
  • 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