---
# System prepended metadata

title: Session based ZK identities

---

# Session based ZK identities

ZK suffers from the same problem as crypto generally: private keys are king. It's impossible to build robust web applications using the private key (single master secret) model. If a user loses their private key, or has it compromised, the account is **permanently** lost. _The javascript browser execution environment cannot be considered safe enough to handle a private key._

In web authentication users expect session based authentication with the ability to

- view active sessions
- log out all other sessions
- create a new session via offline recovery

Most importantly, the account must be recoverable if the browser stored tokens are compromised or lost.

Luckily with ZK we have the flexibility to design a system to do this. This document is loosely based on the terminology of semaphore, but otherwise shares little with the semaphore key structure.

## High level goals

- Recoverable accounts: if a session token is compromised the user can always regain control
- Easy detection: the user should easily be able to see compromised tokens
- Low friction: the user shouldn't have to store or manipulate sensitive keys beyond backup codes
- ZK compatible: external protocols should be able to use the system with minimal complexity

## Semaphore sessions

This system is built with the following requirements:

1. Allow a user to have multiple active session tokens
2. Allow a user to see the last time a token was used
3. Allow a user to disable one or many session tokens
4. Allow a user to recover their account if all sessions have been compromised or lost
    - Using backup codes similar to github/npm/etc 2fa recovery
5. Allow any session token to be used directly in a ZK proof interchangeably
6. Provide an identity secret that is common to all session tokens


A user identity structure might look like this:

```ts
{
    pubkey: uint256,
    backupTreeRoot: uint256,
    identityRoot: uint256
}
```

### List of terms

**pubkey**: A constant public identifier for the identity. This does not change.
**commitment**: The hash of the current identity secret.
**s0**: A secret share that can be combined with any session token to form the identity secret. This changes when a backup code is used.
**sessionTreeRoot**: The root of the session tree. Each leaf of this tree is the hash of a session token. Each session token is a secret share. Combine any token with **s0** to calculate the secret.
**backupTreeRoot**: The root of a tree containing all backup codes. This does not change.
**identityRoot**: `H(pubkey, H(sessionTreeRoot, H(S, s0, shareCount)))` - used as a single identifier for the identity state.
**secret (S)**: A cryptographically strong identity secret. May change over time. Based on `s0` and a `sessionToken`.
**share count**: The number of shares that have been created for the current `s0` value. This value is initialized to 3 (2 shares are created upon registration).

A user can calculate the secret by calculating the slope of a line that intercepts both `s0` and a `sessionToken`. The y intercept of this line is the identity secret. `s0` and all session tokens for an identity should exist on the same line. (See [Shamir's secret sharing](https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing))

### Operations

Given the above structure we can start to define operations.

### Sign up/Register

A user registers by building a ZK proof that accepts the following inputs:
- `s0`: The initial secret share, should be a strong random value
- `sessionToken`: The initial session token, should be a strong random value
- `backupTreeRoot`: The root of a merkle tree filled with random elements. Alternatively this root could be constructed in ZK (with all backup codes being inputs).

Given these inputs the `H(sessionToken)` will be inserted into an empty tree to determine the `sessionTreeRoot`. The following values will be revealed:
- `H(sessionTreeRoot, H(S, s0, 3))`: used to calculate the identity root onchain. The value `3` is the initial share count value. This will be incremented each time a new session token is created.
- `backupTreeRoot`: Used for recovery

Onchain this proof will be verified and a _unique_ `pubkey` should be assigned to the new identity. The `identityRoot` will be calculated by hashing the assigned `pubkey` with the output hash above. The `backupTreeRoot` should be stored and associated with the `pubkey`. The user can now make auth proofs using `sessionToken`.

The contract should maintain a merkle tree of all identitity roots to allow for anonymous authentication. All examples in this document use non-anonymous authentication but can be extended to be anonymous - with the exception of account recovery operations.

### Authenticate

A user authenticates with a `pubkey` by proving the pre-image of a leaf in the session tree.

This proof accepts the following inputs:
- `sessionToken`: The token the user is authenticating with
- `sessionTreeIndices`: Merkle proof indices
- `sessionTreeSiblings`: Merkle proof siblings
- `pubkey`: The identity public key
- `s0`: The initial secret share

The proof should prove that `H(sessionToken)` exists in a tree with root `sessionTreeRoot`. The identity root should be output as a public signal.

This proof is verified by checking that the output matches a valid onchain identity root.

### Sign in a new session

A user signs in a new session using either an existing session or a backup code.

#### Signing in from an existing session

The existing session calculates the current identity secret `S` and shares this with the new session context.

The user then makes a proof with the following inputs:
- `S`: The current identity secret
- `s0`: The current initial secret share
- `sessionToken`: The new session token to activate
- `sessionTreeIndices`: Merkle proof indices
- `sessionTreeSiblings`: Merkle proof siblings
- `pubkey`: The identity public key
- `oldSessionTreeRoot`: The current session tree root (without the new session token)

The proof will verify that `S`, `s0`, and `sessionToken` all exist on the same line. If they do `H(sessionToken)` will be inserted into the `sessionTree` and an updated identity root will be revealed.

This proof should output the old identity root (using `oldSessionTreeRoot`), and a new identity root. The new identity root should only be accepted if the old identity root is valid.

#### Signing in using a backup code

If all session tokens have been lost or are otherwise inaccessible a backup code can be used to create a new session token.

The user makes a proof with the following inputs:
- `s0`: A _new_ initial session share
- `sessionToken`: A _new_ session token to activate
- `pubkey`: The identity public key
- `backupCode`: A valid, unused, backup code
- `backupTreeIndices`: Merkle proof indices
- `backupTreeSiblings`: Merkle proof siblings

The proof will calculate a new `sessionTreeRoot` containing only `sessionToken`. **All existing tokens will be de-activated.** A new identity secret is calculated based on `s0` and `sessionToken`.

The proof outputs a new identity root, and a `backupCodeNullifier` (`H(backupCode`).

To verify this proof the `backupCodeNullifier` must not have been seen before.

If the proof is valid the existing identity root for `pubkey` should be replaced.

### Signout a single session

A user can choose to de-activate a single session token by authenticating and then updating the `sessionTreeRoot` in ZK, removing an entry.

### Signout all other sessions

A user can sign out all other sessions by building a ZK proof that proves authorization and updates the `sessionTreeRoot` to include a single entry: the current session token.

## Open questions

How to get historical identity secrets?
- Symmetrically encrypted linked list of secrets
- Secrets change when the session tree is reset
- Encode in backup codes

Can `backupTreeRoot` function as `pubkey`?

## State of this document

This document is a basic description of a functional protocol. Some notes about data availability (for constructing merkle proofs) are not yet included.