---
id: TIP-1053
title: Witnesses in Key Authorizations
description: Add an optional 32-byte witness to key authorizations so a single signature can both authorize an access key and bind to an arbitrary application context, with manual witness burning for revocation.
authors: Jake Moxey (@jxom)
status: Approved
related: TIP-1011, TIP-1020
protocolVersion: T5
---

# TIP-1053: Witnesses in Key Authorizations

## Abstract

This TIP extends the `key_authorization` payload with an optional `witness: bytes32` field. The protocol includes the witness in the signing hash and emits it when the key authorization is registered, but does not consume it on the happy path. Applications use the field to bind a single signature to an arbitrary context — most commonly a server-issued challenge — so a user can authorize an access key and prove identity to an application in one signature. The same field doubles as a revocation handle for previously signed but not-yet-submitted authorizations: an account can manually burn the witness before the authorization lands onchain.

## Motivation

A common UX pattern requires two signatures from the user:

1. **Authenticate to an application server.** The server issues a random challenge; the user signs it with their account key to prove possession.
2. **Authorize a new access key.** The user signs a `key_authorization` so the application can act on their behalf going forward (subject to limits/scopes/expiry).

On WebAuthn keys, each signature is a separate biometric prompt. Doing this twice for what users perceive as a single "log in" action is awkward and erodes trust.

Adding a `witness` to `key_authorization` lets the user sign **once**: the signature simultaneously authorizes the access key and binds to the offchain verifier's challenge. An offchain verifier (most commonly an application server or any other party that wants to validate the user's intent) checks the binding by recomputing the expected witness from the challenge it issued and comparing it to the value committed to in the signed authorization.

A `witness` field also gives accounts a first-class revocation primitive: an account can burn a witness onchain to invalidate any previously signed but unsubmitted `key_authorization` that uses that same witness. This is particularly useful when a user signs a key authorization that is then lost, leaked, or superseded before it lands on chain.

The protocol stays minimal: it does not learn about sign-in semantics, application schemas, or challenge formats. Apps and wallets standardize on the witness format. The protocol's only added responsibility is checking whether a witness was explicitly burned and exposing witness-bearing registrations through events.

## Assumptions

- The offchain verifier (e.g. app server) is responsible for replay protection of its own session flow. The protocol exposes the witness but does not make it single-use when a key authorization succeeds.
- The `witness` format is application-defined when used as a challenge digest. The protocol does not validate its structure.
- This TIP is purely additive to `key_authorization`. Existing authorizations (without the field) continue to validate identically. When the field is absent, the resulting hash is byte-equivalent to pre-TIP-1053 encoding for that authorization and no witness check or event is performed.
- A manually burned `(account, witness)` prevents future witness-bearing authorizations that use the same pair. Successful witness-bearing authorizations do not burn the witness.

---

# Specification

## Encoding

`key_authorization` gains an optional trailing `witness: bytes32` field:

```
key_authorization = rlp([
  chain_id,
  key_type,
  key_id,
  expiry?,
  limits?,
  allowed_calls?,
  witness?,  // NEW — 32 bytes
])
```

- Encoding is RLP, identical to the existing payload format with one optional trailing item.
- `witness` MUST be exactly 32 bytes when present.
- Absence of the trailing field means "no witness". If the field is present, its exact `bytes32` value is the witness, including `bytes32(0)`.

## Signing

The signing hash is the keccak256 of the canonical RLP encoding:

```
digest = keccak256(rlp(key_authorization))
```

No additional domain separation is introduced. RLP boundaries already disambiguate the trailing field. The protocol verifies signatures exactly as it does today; the only change is that the encoded payload may now contain the additional field.

## Protocol Behavior

The protocol:

- MUST include `witness` in the signing hash whenever it is present in the encoded `key_authorization`.
- MUST reject a witness-bearing `key_authorization` if its `(account, witness)` pair has already been manually burned.
- MUST NOT mark `(account, witness)` as burned or otherwise write witness state on successful authorization.
- MUST emit an event exposing `(account, witness)` when a witness-bearing authorization succeeds.
- MUST NOT enforce ordering, monotonicity, or any other structural constraint on `witness`. Witnesses may be arbitrary 32-byte values chosen by the application.
- MUST NOT interpret the `witness` value beyond checking whether it has been burned.

A witness-less authorization (field absent) is processed exactly as in pre-TIP-1053 behavior: no witness check is performed, no witness event is emitted, and the existing `keys[account][key_id]` permanence is the sole replay guard.

The field is invisible to existing precompile read paths that surface `KeyInfo`. A new read path MAY be added to query burn status of a given `(account, witness)` pair.

## Revocation

An account holder can pre-emptively invalidate a signed-but-unsubmitted `key_authorization` by burning its witness onchain. Burning is a no-op state transition: the protocol exposes a path that marks `(account, witness)` as burned without registering any key. Once the witness is burned, any later attempt to submit the original authorization with the same witness reverts.

This gives users and wallets a clean recovery primitive when:

- A signed authorization is lost or leaked before submission.
- A user wants to supersede an outstanding authorization with a new one for the same `key_id`.

Burning is restricted to the account admin key. Access keys MUST NOT burn authorization witnesses, even if active and otherwise authorized to act on the account's behalf.

## Verification

A offchain verifier (most commonly an application server, but the same flow applies to any party validating the signed authorization) verifies a witness-bearing authorization entirely offchain. The client sends the verifier a single byte string:

```
payload             = key_authorization ‖ signature

key_authorization   = rlp([chain_id, key_type, key_id, expiry, limits, allowed_calls, witness])
signature           = root or admin key signature over keccak256(key_authorization)
```

The verifier:

1. Splits `payload` into `key_authorization` and `signature` (signature length is fixed per signature type).
2. RLP-decodes `key_authorization` to access its fields.
3. Recomputes `expected_witness` from the challenge / typed-data message it issued.
4. Asserts the decoded `witness == expected_witness`.
5. Hashes `key_authorization` with keccak256, recovers the signer from `signature`.
6. Asserts the recovered signer equals the expected account.
7. Validates the remaining decoded fields against the verifier's policy (key_id matches the passkey being registered, expiry is within bounds, limits/scopes match the verifier's expectations).

## Format

The `witness` format is application-defined when used as a challenge digest. The protocol treats any 32-byte value identically and does not prescribe a format. Wallet and application standardization (e.g., a "Sign-In with Tempo" convention) is left to a separate document so it can evolve without protocol changes.

## Backward Compatibility

- Existing key authorizations without the field continue to validate and produce byte-equivalent encodings to pre-TIP-1053 payloads.
- Legacy verifiers that don't understand the field will compute the same hash for legacy-shaped authorizations and will reject (signature mismatch) for authorizations with a present witness, which is the correct fail-safe.
- Witness burn checks are only engaged when the witness field is present; legacy witness-less authorizations incur no additional state reads or writes.

# Invariants

1. **Hash inclusion.** When `witness` is present in the encoded `key_authorization`, it MUST be part of the signing hash.

2. **Encoding determinism.** An absent `witness` field MUST be omitted from the RLP encoding. A present `witness` field MUST encode its exact 32-byte value.

3. **Manual burn.** For any account `A` and any present `witness` value `w`, a successful burn of `(A, w)` MUST cause later `key_authorization` registrations carrying `(A, w)` to revert.

4. **No happy-path consumption.** A successful witness-bearing `key_authorization` MUST NOT burn `(account, witness)`.

5. **Admin-only burn.** A witness burn MUST fail when submitted through an access key.
