Stream storage
StreamSdk.create(client, ledger, vault) needs two customer-operated stores: a SigningVault for the session keys, and a SessionLedger for opaque per-stream bookkeeping. Both are keyed by streamId and both transport Uint8Array values. Use in-memory Maps for development; in production, back them with whatever transactional store you already operate.
Two keys, two layers
C2PA live streaming uses a two-tier signing model from C2PA 2.3 §18.25.
| Layer | What signs | Where the key lives | How often it's used |
|---|---|---|---|
| Long-lived org key + X.509 chain | The init manifest (which embeds the session key as a c2pa.session-keys assertion) | Inside the Limbo Signer | Once per init |
| Ephemeral session key | Each fMP4 media segment | In the customer-operated SigningVault | Every segment (sub-millisecond) |
The session key carries a cryptographic binding back to the organization's certificate and a validity window. Invoking the org key once per segment is too slow for live, so the spec authorizes a short-lived key once (signed by the long-lived key), then signs segments locally with the rostered key until it expires.
SigningVault
Session keys are minted and used here. Private bytes never leave the implementation.
interface SigningVault {
mint(): Promise<VaultPublicKey>;
sign(kid: Uint8Array, payload: Uint8Array): Promise<Uint8Array>;
forget(kid: Uint8Array): Promise<void>;
}
interface VaultPublicKey {
kid: Uint8Array; // opaque key handle
publicKeySec1: Uint8Array; // 65-byte 0x04-prefixed ES256 point
}
The SDK never sees private bytes. mint returns a kid (key handle) and the SEC1 public key; the SDK then asks the vault to sign(kid, payload) per segment.
Why the vault isn't Limbo-managed
Session keys are ephemeral, but they remain signing material under the customer's identity for the duration of their validity:
sign(kid, payload)runs per segment and must complete in local milliseconds. A Limbo-hosted vault would put the public internet on the per-segment path.- The session key's binding to your organization is produced at mint time with the session private key. Whoever operates the vault has visibility into that key, and Limbo deliberately avoids holding customer signing material.
- A leaked session key can forge segments under the customer's identity until it expires. The operator accountable for that leak should be the custodian of the key.
- Customers typically already mint short-lived keys (Vault Transit, AWS KMS data keys, HSM-derived ephemeral keys). The
SigningVaultinterface is three methods (mint,sign,forget) wrapping that infrastructure.
SessionLedger
The ledger holds the SDK's per-stream state as opaque bytes.
interface SessionLedger {
put(id: string, bytes: Uint8Array): Promise<void>;
get(id: string): Promise<Uint8Array | null | undefined>;
forget(id: string): Promise<void>;
}
The ledger bytes are an internal serialization the SDK owns. Do not inspect or construct them.
Why the ledger isn't Limbo-managed
- Atomicity is already provided by the customer's transactional store (Redis
WATCH/MULTI, PostgresSERIALIZABLE, DynamoDB conditional writes). A Limbo-hosted ledger would be a strictly weaker hop in front of it. - It is on the per-segment path. The latency argument is the same as the vault.
- The bytes are an internal serialization that may change between SDK versions. The customer's store handles bytes; it does not require a schema migration when Limbo ships a fix.
Single writer per stream id
The SDK assumes a single writer per streamId for the lifetime of a stream. If the encoder pipeline can fan out, serialize calls behind a per-id mutex. Different streamId values are fully independent.
See also
- Sign a live stream: the
StreamSdklifecycle. - Key rotation: re-init mints a fresh key and forgets the old one.
- Live TV broadcast: vault and ledger wired into a full pipeline.