# Encrypted Authorship Verification (EAV) — Specification

**Version:** `redstet-eav:v1.0`
**Status:** Draft
**Last updated:** 2026-06-03 (amended for Sprint B Week 1 — Sigstore Rekor anchoring landed; anchor shape + asynchronous-anchoring note in §4)

EAV is a protocol for cryptographically tying a piece of writing to its keystroke-by-keystroke creation history, in a form that's:

- **Tamper-evident** — any post-hoc edit invalidates the proof
- **Encrypted at rest** — the recording content is unreadable without a verification path
- **Externally verifiable** — anyone with the verifier tool can confirm the record's authenticity
- **Forward-portable** — the encryption scheme upgrades cleanly without re-encrypting the underlying events

Red Stet is the reference implementation. The spec is open so future writing tools can adopt it (or compete with their own implementations of the same protocol).

---

## 1. Scope

### In scope
- Envelope format for an EAV record
- Signature scheme over the envelope
- Encryption scheme for the recorded events
- Key derivation and wrapping
- Verification flow
- Upgrade and migration semantics
- Conformance requirements for an EAV-compliant tool

### Out of scope
- The recording mechanism itself (what events are captured, how the editor instruments keystrokes — that's the writing-tool implementation)
- The user interface that displays a verified record
- Key escrow business arrangements (which third-party agent, what jurisdiction, etc.)
- Authentication of the viewer (Clerk, OAuth, password — implementation-specific)

---

## 2. Terms

- **Record** — a single completed writing session (one keystroke stream + metadata + signatures)
- **Envelope** — the on-the-wire container holding a record
- **Events** — the captured keystroke + paste + focus stream inside the envelope
- **DEK** (Data Encryption Key) — the per-record symmetric key that encrypts the events
- **KEK** (Key Encryption Key) — the per-user or per-scope key that wraps (encrypts) the DEK
- **Wrapping** — encrypting a key with another key (DEK gets wrapped by KEK)
- **Signing key** — the asymmetric key the producer (e.g. Red Stet) uses to sign envelopes
- **Verification key** — the public half of the signing key, published for verifiers
- **Anchor** — an external attestation (timestamp authority, transparency log, blockchain) corroborating that the envelope existed at a given time
- **Verifier** — the tool that confirms an envelope's authenticity (Red Stet's `/verify` page, or any conformant implementation)

---

## 3. Architecture

EAV is a layered stack:

```
┌─────────────────────────────────────────────────┐
│ Layer 4: Anchor (timestamp / transparency log)  │  External corroboration
├─────────────────────────────────────────────────┤
│ Layer 3: Signature (ES256 / JWS)                │  Producer attestation
├─────────────────────────────────────────────────┤
│ Layer 2: Envelope encryption (DEK wrapped by KEK) │  Confidentiality
├─────────────────────────────────────────────────┤
│ Layer 1: Event ciphertext (AES-256-GCM)         │  The actual writing record
└─────────────────────────────────────────────────┘
```

Each layer can be upgraded independently:
- Layer 1: the cipher could move to AES-256-OCB or ChaCha20-Poly1305 in a future spec version
- Layer 2: the KEK derivation could move from Clerk-identity to third-party escrow to threshold cryptography
- Layer 3: the signature could move to Ed25519 or post-quantum schemes
- Layer 4: the anchor could be OpenTimestamps, Sigstore Rekor, or a future standard

The envelope schema version (`redstet-eav:v1`, `redstet-eav:v2`, …) coordinates upgrades.

---

## 4. Envelope format

The on-the-wire envelope is a single JSON object. Schema:

```jsonc
{
  // ── Identity ──────────────────────────────────────────────────────
  "schema":      "redstet-eav:v1",         // protocol version
  "envelopeId":  "01HXYZ...",          // ULID; unique per record
  "createdAt":   1717564800000,        // ms epoch
  "producer":    "redstet:1.0",       // implementing tool + version

  // ── Subject ──────────────────────────────────────────────────────
  "docId":       "k1abc...",           // the document this record covers
  "ownerUserId": "k2def...",           // the writer's user id (producer-scoped)

  // ── Window ───────────────────────────────────────────────────────
  "startedAt":   1717564000000,
  "endedAt":     1717564780000,
  "eventCount":  4218,

  // ── Key envelope (Layer 2) ───────────────────────────────────────
  "keyEnvelope": {
    "scheme":      "clerk-identity-hkdf-v1",   // KEK derivation scheme
    "kdfParams":   { "salt": "...", "info": "redstet-eav:v1:dek" },
    "wrappedDek":  "base64-aes-256-gcm-ciphertext-of-dek",
    "wrappedDekIv": "base64-12-byte-iv",
    "wrappedDekTag": "base64-16-byte-auth-tag",
    "grants":      [
      // optional: additional KEK wrappings (one per authorized viewer beyond owner)
      // populated lazily when a teacher / class staff is granted access
      { "kekScheme": "clerk-identity-hkdf-v1",
        "kekRef":    "clerk-user-id-of-grantee",
        "wrappedDek": "...", "iv": "...", "tag": "..." }
    ]
  },

  // ── Encrypted payload (Layer 1) ──────────────────────────────────
  "ciphertext": {
    "alg":  "AES-256-GCM",
    "iv":   "base64-12-byte-iv",
    "tag":  "base64-16-byte-auth-tag",
    "data": "base64-ciphertext-of-events-array"
  },

  // ── Signature (Layer 3) ──────────────────────────────────────────
  "signature": {
    "alg":     "ES256",                                    // ECDSA P-256 + SHA-256
    "kid":     "redstet-eav-2026",                        // signing key id
    "jws":     "eyJhbGciOiJFUzI1NiIsImtpZCI6Li4ufQ..."     // JWS over the canonical hash
  },

  // ── Anchor (Layer 4) — optional in v1, recommended ───────────────
  // Producer note: the on-disk envelope blob does NOT contain `anchor`.
  // Anchoring runs asynchronously after finalize (the Rekor round trip
  // is ~1-3s; the user's call should not wait for it), so the anchor
  // metadata is stored alongside the envelope in the producer's row
  // (e.g. `provenanceRecordings.eavAnchor` in Red Stet) and merged into
  // the envelope at verification time. This is why `anchor` is excluded
  // from the canonical hash — it doesn't exist at signing time.
  "anchor": {
    "scheme":      "sigstore-rekor:v1",
    "logIndex":    8429301,                     // index in the Rekor log
    "uuid":        "24296fb24b8ad77a...",      // Rekor entry UUID
    "inclusionProof": {
      "checkpoint":     "...",                 // base64 signed log checkpoint
      "hashes":        ["...", "..."],         // base64 Merkle inclusion path (sibling hashes, root-ward)
      "treeSize":       8429302,
      "rootHash":       "base64-merkle-root-hash"
    },
    "signedEntryTimestamp": "...",              // base64 ECDSA SET over the entry
    "integratedTime":       1717564780,         // seconds epoch from Rekor
    "anchoredAt":           1717564782451       // ms epoch when the producer called Rekor
  }
}
```

### Canonical form for hashing

For the signature to verify deterministically, the envelope is hashed in **canonical form**: the JSON object serialized with sorted keys, no whitespace, UTF-8 encoded, with the `signature`, `anchor`, AND `keyEnvelope` fields removed. SHA-256 of that canonical form is what the JWS signs.

The `keyEnvelope` exclusion is deliberate (added in the 2026-06-03 amendment). The signature attests to the immutable ciphertext + identity metadata. The `keyEnvelope` is the mutable access-control layer — adding or revoking a grant rewraps the DEK and changes the wrapping bytes, but does not (and must not) change the signed events. Including `keyEnvelope` in the canonical hash would force re-signing on every grant mutation, which is incompatible with the multi-recipient grant model in §6.

The signature still cryptographically binds:
- The envelope identity (`schema`, `envelopeId`, `createdAt`, `producer`)
- The subject (`docId`, `ownerUserId`)
- The window (`startedAt`, `endedAt`, `eventCount`)
- The events ciphertext (which transitively covers the DEK via the GCM auth tag)

Tamper-evidence is preserved because any edit to the events ciphertext invalidates the GCM auth tag, and any edit to the metadata invalidates the signature. The DEK itself is only verifiable via the GCM auth tag at decrypt time — but since the DEK is held by the producer and re-derivable by authorized recipients, the protocol does not need the signature to attest to the wrapped form.

This is necessary because JSON parsers may reorder keys; canonicalization makes the hash reproducible across implementations.

Reference canonicalization: [RFC 8785 — JSON Canonicalization Scheme (JCS)](https://datatracker.ietf.org/doc/html/rfc8785).

### Events array (decrypted)

After Layer 2 unwraps and Layer 1 decrypts, the cleartext is a JSON array of event objects:

```jsonc
[
  { "t": 0,    "type": "session-start", "doc": "k1abc..." },
  { "t": 142,  "type": "keystroke",     "k": "h", "pos": 0 },
  { "t": 318,  "type": "keystroke",     "k": "e", "pos": 1 },
  { "t": 4012, "type": "paste",         "pos": 87,  "len": 312 },   // paste position + length, NOT content
  { "t": 4500, "type": "focus-blur",    "blurred": true },
  { "t": 8200, "type": "focus-return",  "blurred": false },
  // ...
]
```

Event types and required fields are defined in `EAV_EVENTS.md` (the events schema; separate doc).

---

## 5. Signature scheme (Layer 3)

### Algorithm
- **ES256** (ECDSA over the P-256 curve with SHA-256), per [RFC 7518](https://datatracker.ietf.org/doc/html/rfc7518)

### Signing key
- Held by the producer (Red Stet)
- The public key is published as a JWK at a well-known URL (Red Stet publishes at `/.well-known/jwks.json`)
- Key rotation: producers may rotate signing keys; each envelope's `signature.kid` references the key version used. Verifiers fetch the matching public key from the JWKS.

### Fields excluded from the canonical form

The canonical hash is computed over the envelope MINUS the following fields:

- `signature` — the JWS itself, recursive otherwise
- `anchor` — added AFTER signing by Layer 4
- `keyEnvelope` — the mutable wrapping layer (see §4 "Canonical form for hashing" for full rationale)

All other fields participate in the canonical hash.

### Signing flow
1. Construct the envelope JSON with all fields except `signature`, `anchor`, and `keyEnvelope`
2. Canonicalize via JCS (RFC 8785)
3. Compute SHA-256 hash of the canonical form
4. Sign the hash with the producer's private signing key (ES256)
5. Set `signature.alg`, `signature.kid`, `signature.jws` accordingly
6. Attach `keyEnvelope` (with the owner's wrapped DEK + any initial grants); attaching after-the-fact does not invalidate the signature

### Verification flow
1. Extract `signature` from the envelope
2. Canonicalize the remaining envelope (without `signature`, without `anchor`, without `keyEnvelope`)
3. Fetch the producer's public JWK using `signature.kid`
4. Verify the JWS against the canonical hash
5. If verification fails, the envelope is rejected as inauthentic

---

## 6. Encryption scheme (Layers 1 + 2)

### Layer 1 — content encryption (per envelope)

- **Algorithm:** AES-256-GCM
- **Key:** Per-envelope DEK (data encryption key), 256-bit, generated using a cryptographically secure random source
- **IV:** 12-byte random per encryption
- **Authentication tag:** 16-byte GCM auth tag

The events array is JSON-serialized, UTF-8 encoded, then encrypted under the DEK with the IV. The ciphertext + IV + auth tag go into `ciphertext`.

### Layer 2 — key wrapping (per viewer)

The DEK is wrapped (encrypted) under one or more KEKs. The wrapped DEK is stored in `keyEnvelope`. Each wrapping is an independent unwrap path — any party with a matching KEK can recover the DEK.

This pattern is called **envelope encryption** and is the same model AWS KMS, Google Cloud KMS, and most cloud-native crypto systems use. The advantage: **changing KEKs does not require re-encrypting the events.** Only the wrapped DEK changes.

#### v1 KEK derivation: `clerk-identity-hkdf-v1`

In `redstet-eav:v1`, the KEK is derived deterministically from the user's authenticated identity (Clerk user ID) and a server-held salt:

```
KEK = HKDF-SHA256(
  ikm  = utf8("clerk-user:" + clerk_user_id),
  salt = RED_STET_KEK_SALT,                // server env var, 32 bytes
  info = utf8("redstet-eav:v1:kek"),
  L    = 32                                 // bytes
)
```

Where:
- `clerk_user_id` is the user's Clerk identifier, obtained from a verified Clerk session JWT
- `RED_STET_KEK_SALT` is a 32-byte secret held in Red Stet's Convex environment (`RED_STET_KEK_SALT`)
- The KDF is HKDF-SHA256, per [RFC 5869](https://datatracker.ietf.org/doc/html/rfc5869)

Security properties:
- A user's KEK is derivable only by a party holding BOTH the Clerk identity (proven via session) AND the server salt
- The server never persists the KEK; it derives on demand at view-time and discards
- A Clerk identity alone is insufficient (lacks the salt)
- The salt alone is insufficient (lacks the Clerk identity)

#### v1 wrapping

Once a KEK is derived for a recipient (owner or grantee), the DEK is wrapped:

```
wrappedDek = AES-256-GCM(
  key       = KEK,
  iv        = random_12_bytes(),
  plaintext = DEK
)
```

The wrapping yields a ciphertext + IV + auth tag, stored as a `keyEnvelope` entry.

#### Multi-recipient grants

A record may need to be accessible to multiple authenticated users (the student who wrote it, their teachers, possibly co-teachers / TAs). For each authorized recipient:

1. Producer derives the recipient's KEK using their Clerk identity
2. Producer wraps the same DEK with that KEK
3. The wrapped DEK is appended to `keyEnvelope.grants[]` with a `kekRef` identifying the grantee

Authorization of grant creation is an implementation concern — see §10 for Red Stet's authorization model.

#### Forward upgradeability

When EAV upgrades from `clerk-identity-hkdf-v1` to a future scheme (e.g. `threshold-shamir-v1` or `iron-mountain-escrow-v1`):

1. The server holds the master KEK derivation under both old and new schemes during the migration window
2. For each existing record, the server unwraps the DEK with the v1 KEK
3. The server re-wraps the DEK with the new-scheme KEK
4. The `keyEnvelope.scheme` field is updated to the new value
5. The Layer 1 ciphertext is **untouched**

Because the DEK does not change, the encrypted events do not need re-encryption. A re-wrap pass over the wrapped DEKs is sufficient. This is the upgrade-without-data-loss property.

---

## 7. Anchor (Layer 4) — optional but recommended

The anchor is an external attestation that the envelope existed at a specific time, independent of the producer.

### Why
The signature in Layer 3 proves the envelope was created by the producer. It does NOT prove WHEN it was created — only that the producer claims a given `createdAt`. A producer that's compromised or coerced could backdate envelopes.

The anchor adds a timestamp from an independent third party (or many parties, in the case of public blockchains) that cannot be backdated. A verifier checks both the signature and the anchor; both must validate for the envelope to be trusted.

### v1 recommended scheme: `sigstore-rekor:v1`

- The producer hashes the canonical signed envelope (SHA-256)
- The hash is submitted to the [Sigstore Rekor](https://www.sigstore.dev/) transparency log as a `hashedrekord` entry
- Rekor appends the entry to its Merkle tree, returns a log index + UUID + inclusion proof against a signed checkpoint
- The inclusion proof + checkpoint are stored in `envelope.anchor`

Why Sigstore Rekor:
- Backed by Google + Red Hat + the OpenSSF — an enterprise-credible governance model schools and procurement teams recognize
- Transparency-log design: append-only, Merkle-tree-backed, periodically witnessed and checkpointed by independent parties
- Free to use; the public Rekor instance has been live since 2021 and serves the container-signing ecosystem (Cosign / Sigstore is the de facto standard for OSS software supply chain)
- Verification doesn't depend on Red Stet's infrastructure — the public Rekor log + Sigstore witnesses are the trust root

Verification:
- The verifier extracts the inclusion proof + checkpoint from `envelope.anchor`
- Fetches the current Rekor public key (or pins it for offline verification)
- Verifies the Merkle inclusion proof against the checkpoint's root hash
- Confirms the checkpoint's signature against the Rekor public key
- The `integratedTime` from the Rekor entry is the proof-of-existence timestamp

### Alternative schemes (informative)

Producers MAY implement other anchor schemes. Verifiers MUST clearly indicate when they encounter an anchor scheme they don't support. Candidates:

- `opentimestamps:bitcoin` — Bitcoin-anchored timestamping via OpenTimestamps. Strong long-term durability; trust root is Bitcoin's hash chain. Less enterprise-readable (some buyers react negatively to "Bitcoin" regardless of the underlying tech).
- `rfc3161:tsa` — RFC 3161 timestamping authority (Sectigo, DigiCert, etc.). Standard enterprise-IT primitive; per-stamp cost; trust root is the TSA's PKI.

The choice is positioning + cost + trust-root. Sigstore Rekor is recommended for `redstet-eav:v1` because the OpenSSF governance reads as enterprise-credible to school IT and district CFO buyers — the audiences most likely to ask about the anchor at all.

### Anchor is optional in v1

Producers MAY ship envelopes without an anchor. Verifiers MUST display whether an anchor is present and validated. A signed-but-not-anchored envelope is still cryptographically valid for the producer's claim; the anchor adds defense against producer compromise.

Recommendation: producers SHOULD anchor every finalized envelope. Anchoring is cheap (Sigstore Rekor is free) and the trust-strengthening benefit is substantial.

---

## 8. Verification flow

A verifier (web page, app, or third-party tool) takes an envelope and an authenticated viewer identity, and returns either:
- The decrypted events array + verification metadata (signatures, anchor proofs), or
- An error indicating which check failed

### Steps

1. **Parse envelope.** Reject if not valid JSON or missing required fields.
2. **Schema check.** Confirm `schema` is a recognized version. Reject if unknown.
3. **Signature verification (Layer 3).**
   - Fetch producer's JWKS, locate the key matching `signature.kid`.
   - Canonicalize the envelope (excluding `signature`, `anchor`, and `keyEnvelope` fields — see §5).
   - Compute SHA-256 hash of the canonical form.
   - Verify `signature.jws` against the hash using the public key.
   - If verification fails, the envelope is rejected as inauthentic.
   - Note: the signature covers the events ciphertext + identity metadata. It does NOT cover `keyEnvelope` — that layer is verified separately at unwrap time (a wrong KEK or revoked grant fails at GCM auth-tag verification in step 6).
4. **Anchor verification (Layer 4, if present).**
   - If `anchor` is present, validate its proof.
   - For `sigstore-rekor:v1`: verify Merkle inclusion proof against the checkpoint root; verify checkpoint signature against Rekor public key; surface `integratedTime` as the anchor timestamp.
   - For other supported schemes, follow their respective verification flows. Reject envelopes with unsupported anchor schemes with a clear message.
   - Display the anchor's timestamp in the verification UI.
5. **Authorization (Layer 2).**
   - Confirm the viewer's authenticated identity has a `keyEnvelope` entry (owner or grant).
   - If no entry, the viewer is not authorized; return a permission error.
6. **DEK unwrap (Layer 2).**
   - Derive the viewer's KEK per the `keyEnvelope.scheme`.
   - Unwrap the matching wrapped DEK using the KEK.
   - If unwrap fails (auth tag mismatch), the envelope is rejected.
7. **Events decryption (Layer 1).**
   - Decrypt `ciphertext.data` using the DEK + `ciphertext.iv`.
   - Verify the GCM auth tag.
   - Parse the cleartext as a JSON events array.
8. **Return.**
   - Events array.
   - Verification metadata: signature valid, anchor valid (or absent), KEK scheme, anchor timestamp.
   - The verifier UI displays the timeline and the verification badges.

### Failure modes

| Check fails | What it means |
|---|---|
| Signature | Envelope was tampered with after signing; reject |
| Anchor | Producer claims a time the anchor doesn't corroborate; flag for review |
| Authorization | Viewer is not authorized; return permission error |
| DEK unwrap | Viewer holds the wrong KEK, or grant was revoked; permission error |
| Events decryption | Ciphertext was corrupted or auth tag wrong; reject |

A verifier MUST clearly distinguish between "this envelope was tampered" (signature fail), "this viewer can't see it" (auth fail), and "this envelope was corrupted in transit" (ciphertext fail). These imply different remedies.

---

## 9. Compatibility, upgrades, and key rotation

### Schema version

Each envelope declares `schema: "red-eav:vN"`. Verifiers MUST reject envelopes with an unknown schema version. New schema versions MAY change layer algorithms, field shapes, or add layers; they MUST NOT silently break verification of older envelopes.

### Signing key rotation

Producers MAY rotate signing keys at any cadence. Each envelope's `signature.kid` identifies the key version used. The producer's JWKS endpoint MUST serve all currently-valid public keys (current + recently-rotated). A key MAY be deprecated after a deprecation window during which existing envelopes signed under it remain verifiable.

Recommendation: signing key rotation every 12 months; deprecated keys retained in JWKS for 7 years (matching common document-retention windows).

### KEK derivation upgrade

Producers MAY upgrade the KEK derivation scheme. The upgrade flow:

1. New scheme is defined and reviewed (e.g. `threshold-shamir-v1`)
2. For each existing record:
   a. Derive the v1 KEK (Clerk-identity-hkdf) using the existing user identity and salt
   b. Unwrap the existing wrapped DEK
   c. Derive the new scheme's KEK for the same authorized recipients
   d. Wrap the same DEK under each new KEK
   e. Replace `keyEnvelope` with the new wrappings; set `keyEnvelope.scheme` to the new value
3. The Layer 1 ciphertext is unchanged
4. Re-sign the envelope to attest the new `keyEnvelope` (signature covers the whole envelope, so re-signing is required)

This migration can be done in the background, batched. Users experience no service interruption. The events ciphertext (the bulk of the data) is unaffected.

### Anchor scheme upgrade

Producers MAY change anchor schemes between envelopes. Verifiers MUST support multiple anchor schemes (or reject envelopes with unsupported anchor schemes with a clear message). Existing anchors are NOT re-anchored on upgrade; they remain valid against their original anchor.

---

## 10. Authorization model (Red Stet implementation)

This section is Red Stet-specific; other EAV implementations MAY use different authorization rules.

A user has access to an EAV record if any of:

1. **Owner** — they are the record's `ownerUserId` (the student who wrote it)
2. **Class staff** — they hold a non-removed `classroomStaff` row on the classroom that owns the assignment + a capability that includes recording-view (`gradeSubmissions` or `viewIntegritySignals`)
3. **Explicit grant** — they have been added to `keyEnvelope.grants[]` by the owner or by Red Stet's compliance-export flow

Grant creation:
- When a `classroomStaff` row is added (teacher invited as co-teacher), Red Stet's server eagerly re-wraps the DEKs of all existing records the new staff member should now have access to
- When a `classroomStaff` row is removed, the grant is removed from future verifications. Note: this does NOT retroactively prevent access if the grantee previously decrypted; the cryptographic horse has left the barn. The mitigation is to rotate the DEK (which requires re-encrypting the events) — this is offered as a "lock from staff" mutation but should be used sparingly.

### Compliance export (org admin path)

Org admins (district / school) can request a compliance export under §1c of `docs/PRIVACY.md`. This path:

1. Server verifies the admin's authority
2. Server unwraps the DEKs for the records covered by the export
3. Re-wraps the DEKs under a one-time compliance KEK
4. Bundles the encrypted records + the compliance KEK reference into a sealed export package
5. Fans out privacy notices to every affected student (per the PRIVACY.md compliance-export-affected notification)

The export package is decryptable only by the requesting admin using the compliance KEK. The KEK is shipped via a separate channel (secure download with TTL).

---

## 11. Conformance — what makes a tool EAV-compliant

A tool is EAV-compliant for the producer role if it:

1. Generates envelopes matching §4 (envelope format)
2. Signs envelopes per §5 (ES256 / JWS over canonical JSON)
3. Encrypts events per §6 (AES-256-GCM + DEK + KEK envelope encryption)
4. Publishes a JWKS endpoint for signing-key public keys (Section §5)
5. Supports at least one KEK derivation scheme (Clerk-identity-hkdf-v1 in this spec; future schemes are additive)
6. (Recommended) Anchors envelopes per §7

A tool is EAV-compliant for the verifier role if it:

1. Parses envelopes per §4
2. Validates signatures per §5
3. Validates anchors per §7 (or clearly indicates unsupported anchor schemes)
4. Implements at least one KEK derivation scheme to unwrap and decrypt
5. Distinguishes failure modes per §8

A claim of EAV compliance for either role requires the tool's verifier source to be auditable. Red Stet publishes its verifier source at `https://github.com/Moikeha09/redstet` under `verify/` (planned).

---

## 12. Threat model

### Protects against

- **Forgery** — an attacker cannot produce a valid signature without the producer's private signing key
- **Tampering** — any post-hoc edit to the envelope invalidates the signature
- **Casual reading** — an attacker who obtains the envelope file cannot read the events without a KEK
- **Producer backdating** — if anchored, the envelope's claimed timestamp is corroborated by an independent third party
- **Storage compromise** — Convex File Storage breach reveals only ciphertext; the KEK salt is in a separate secret

### Does NOT protect against

- **Producer key compromise** — if Red Stet's signing key is stolen, an attacker can forge envelopes. Mitigation: HSM-stored signing key + frequent rotation
- **KEK salt compromise** — if `RED_STET_KEK_SALT` leaks, all KEKs become derivable. Mitigation: HSM-stored salt + threshold-cryptography upgrade
- **Authorized viewer leaking** — a teacher who can legitimately decrypt can also legitimately save / export / screenshot. Mitigation: in-product viewing only (no raw event export to authorized viewers), audit logging of all access
- **Client-side compromise** — if a viewer's browser is compromised, the cleartext events can be exfiltrated at decryption time. Mitigation: same as any browser-based decryption; out of scope for the spec
- **Coercion of producer or escrow agent** — a legal compulsion (subpoena, court order) MAY require the producer or escrow agent to decrypt records. EAV is not a tool to defeat legal process; it is a tool to make legitimate verification cryptographically sound

---

## 13. Open implementation

Red Stet publishes:

- This spec (`docs/EAV_SPEC.md`)
- The events schema (`docs/EAV_EVENTS.md`, planned)
- The producer-side implementation (`convex/provenance.ts`, `convex/signing.ts`, planned `convex/eav.ts`)
- The verifier-side implementation (`verify/index.html`, planned to move to a separate audited bundle)
- The JWKS endpoint at `https://red-stet.convex.cloud/.well-known/jwks.json`

The verifier source is open-source under the MIT license (planned). Anyone can host their own verifier; the protocol does not require Red Stet's infrastructure to validate envelopes (though Layer 2 unwrap requires either Red Stet's KEK salt or a future third-party-anchored KEK scheme).

---

## 14. Open questions for v2

The following items are deliberately deferred from `redstet-eav:v1`:

1. **Third-party key escrow** — design the protocol for an external escrow agent to hold a fraction of the KEK derivation material, enabling continuity if Red Stet ceases operations
2. **Threshold cryptography** — split the KEK derivation across N parties (Red Stet + escrow + accreditor) requiring K of N to decrypt
3. **Post-quantum signatures** — migrate Layer 3 to ML-DSA (NIST FIPS 204) or similar before practical quantum attacks emerge
4. **Cross-producer interop** — define a registry of EAV-compliant producers so a verifier can validate envelopes from multiple writing tools without knowing the producer in advance
5. **Selective disclosure** — allow a viewer to confirm specific facts about the record (e.g. "this was written over 4 hours, no paste larger than 50 chars") without revealing the full event stream. Zero-knowledge proof scheme to be specified.

---

## 15. Implementation roadmap (Red Stet, this codebase)

### Phase EAV-1 — encryption layer (v1 of this spec)

1. **Schema additions** in `convex/schema.ts`:
   - `provenanceRecordings.eavKeyEnvelope` (the wrappedDek + grants — stored in the row, NOT in File Storage, because it's mutable)
   - `provenanceRecordings.eavSchema` ("redstet-eav:v1")
   - Existing `events` blob in File Storage becomes the EAV envelope JSON (ciphertext + signature, NO keyEnvelope)

2. **Producer pipeline** in `convex/provenance.ts`:
   - `generateDek()` — crypto.subtle 256-bit AES-GCM key
   - `deriveKek(userId)` — HKDF from Clerk identity + `RED_STET_KEK_SALT`
   - `wrapDek(dek, kek)` — AES-GCM wrap
   - `encryptEvents(events, dek)` — AES-GCM encrypt
   - `finalizeEavEnvelope(...)` — full envelope construction with signature

3. **Verifier pipeline** in `verify/index.html` + `convex/eavVerify.ts`:
   - Fetch envelope by `publicSlug` or signed download URL
   - Verify signature (existing flow extends)
   - Authenticate viewer (Clerk session or verifier-link token)
   - Derive viewer's KEK
   - Unwrap DEK from `keyEnvelope`
   - Decrypt and render events

4. **Grant management** in `convex/eavGrants.ts` (new):
   - On `classroomStaff` invite, eagerly re-wrap existing DEKs for the new staffer
   - On `classroomStaff` remove, mark grants as revoked (note: revocation is forward-looking, not retroactive)
   - Compliance-export grant path

5. **Migration** for existing recordings:
   - Background job that reads each existing `provenanceRecordings.eventsStorageId` blob
   - Generates a fresh DEK
   - Re-encrypts the events under the DEK
   - Wraps the DEK with the owner's KEK + any staff grants
   - Writes the new ciphertext + envelope
   - Updates the row to mark `eavSchema: "redstet-eav:v1"`
   - Old plaintext blobs deleted after a confirmation window

### Phase EAV-2 — anchoring (Layer 4)

Add Sigstore Rekor anchoring at finalize time. Producer submits the canonical signed-envelope hash as a `hashedrekord` entry; stores log index + UUID + inclusion proof + checkpoint in `provenanceRecordings.eavAnchor`. ~3-5 days work after Phase EAV-1.

### Phase EAV-3 — third-party escrow upgrade

Future. Adds a second KEK derivation scheme; runs migration to re-wrap DEKs. ~2 weeks work, no data re-encryption.

---

## 16. References

- [RFC 7518 — JSON Web Algorithms (JWA)](https://datatracker.ietf.org/doc/html/rfc7518) — JWS / ES256
- [RFC 5869 — HKDF](https://datatracker.ietf.org/doc/html/rfc5869)
- [RFC 8785 — JSON Canonicalization Scheme (JCS)](https://datatracker.ietf.org/doc/html/rfc8785)
- [NIST SP 800-38D — AES-GCM](https://csrc.nist.gov/publications/detail/sp/800-38d/final)
- [Sigstore / Rekor](https://www.sigstore.dev/) — recommended anchor scheme for `redstet-eav:v1`
- [OpenTimestamps](https://opentimestamps.org/) — Bitcoin-anchored timestamping (alternative anchor scheme)
- [RFC 3161 — Internet X.509 PKI Time-Stamp Protocol (TSP)](https://datatracker.ietf.org/doc/html/rfc3161) — alternative anchor scheme

---

## 17. Change log

- **2026-06-03 — v1.0 draft** — Initial specification. Awaiting implementation review and external feedback before declaring v1.0 final.
- **2026-06-03 — amendment** — `keyEnvelope` excluded from the canonical signature hash. The original spec (§5) required signing the entire envelope minus `signature` and `anchor`, which made every grant addition or revocation invalidate the existing signature. That was incompatible with the multi-recipient grant model in §6, where teachers / co-teachers / TAs gain access lazily after the record is finalized and signed. The fix: signature attests to the immutable layers (identity metadata + events ciphertext); the `keyEnvelope` is the mutable access-control layer, verified separately at unwrap time via AES-GCM auth-tag. Tamper-evidence over the events is preserved (any edit invalidates the GCM tag, which invalidates the signed envelope). See §4 "Canonical form for hashing" and §5 "Fields excluded from the canonical form" for the updated rules.
- **2026-06-03 — Sprint B Week 1 (EAV-2 anchoring)** — Layer 4 landed. `finalizeEav` now schedules a `hashedrekord` submission to the public Sigstore Rekor instance immediately after the row is inserted, runs out-of-band (the user's call returns without waiting for the ~1-3s Rekor round trip), persists the inclusion proof + checkpoint + signed entry timestamp + log index + UUID on the row, and verifies the proof locally (RFC 9162 Merkle walk) before stamping `eavAnchorStatus: 'success'`. A hourly retry cron picks up rows whose first attempt didn't land (transient Rekor outage, network flake) and re-runs the anchor action; after three failures the row is marked `eavAnchorStatus: 'failed'` with the last error in `eavAnchorError`. The envelope example in §4 was amended to add `signedEntryTimestamp` and `anchoredAt` fields, and to note that the on-disk envelope blob does NOT contain `anchor` — anchor metadata lives alongside the envelope in the producer's row and is merged at verification time. The verifier (`convex/eavVerify.ts`) now returns anchor verification state (`anchorValid`, `anchorScheme`, `anchorTimestamp`, `anchorLogIndex`, `anchorStatus`) on every successful verify. Anchor failure does NOT invalidate the envelope — the signature is still the producer's attestation. The UI surfaces the two layers separately.
