Tearleads

Protocol specification: foundations

Goals, protocol planes, and the common cryptographic objects.

Published from docs/protocol-specification/foundations.md (GitHub)

Part of the protocol specification.

Goals

  • Keep document and blob plaintext opaque to the server.
  • Make access and key-target state client-verifiable.
  • Bind every encrypted content record to a signed write header.
  • Bind every structural access mutation to a signed access event and derived manifest.
  • Keep blob attachment state server-indexable without exposing document plaintext.
  • Let the app fail closed when required cryptographic policy material is missing, stale, or inconsistent.

Protocol Planes

The protocol has three adjacent planes:

The planes are separate but not independent. A document sync write needs access proofs and content-key targets. A blob bind needs a staged encrypted blob, attachment metadata, access proofs, blob content-key targets, and a blob write header.

Common Cryptographic Objects

Signed access events have this logical shape:

type AccessEvent = {
  version: 1;
  eventId: string;
  eventType:
    | "attachment.bind"
    | "attachment.detach"
    | "container.create"
    | "container.grant"
    | "container.move"
    | "container.recite"
    | "container.rekey"
    | "container.revoke"
    | "document.link"
    | "document.purge"
    | "document.unlink";
  objectKind: "blob" | "container" | "document";
  objectId: string;
  organizationId: string;
  previousManifestHash: string | null;
  dependencyManifestHashes: string[];
  bodyHash: string;
  signerUserId: string;
  signerDeviceId: string;
  signerKeyFingerprint: string;
  signedAt: string;
  signature: string;
};

Access manifests commit to the derived state for a container, document link set, or blob target set:

type ReferencedPrincipalHead = {
  principalType: "group" | "organization";
  principalId: string;
  version: number;
  keyEpoch: number;
  stateHash: string;
  keyFingerprint: string;
};

type AccessManifest = {
  version: 1;
  objectKind: "blob" | "container" | "document";
  objectId: string;
  organizationId: string;
  epoch: number;
  previousManifestHash: string | null;
  eventHash: string;
  structuralHash: string;
  grantRoot: string;
  referencedPrincipalHeads: ReferencedPrincipalHead[];
  keyTargetHash: string;
};

Write headers commit encrypted content records to the object, access head, key epoch, target set, ciphertext hash, metadata hash, nonce domain, and writer identity:

type WriteHeader = {
  version: 1;
  organizationId: string;
  objectKind: "blob" | "document";
  objectId: string;
  accessManifestHash: string;
  dependencyManifestHashes: string[];
  contentKeyEpoch: number;
  targetHash: string;
  encryptionSuite: "aes-256-gcm-hkdf-sha256-record-key";
  contentRecordId: string;
  nonceDomainHash: string;
  metadataHash: string;
  ciphertextHash: string;
  writerUserId: string;
  writerDeviceId: string;
  writerKeyFingerprint: string;
  signedAt: string;
  signature: string;
};

The header’s dependencyManifestHashes is the sorted, unique union of all manifest hashes in the complete authorizing container paths. Submission must cite the verified current paths; historical reads rebuild those exact signed paths and use referenced group membership. The API retains the dependencies of retained document and blob headers for fresh-device verification. All signed timestamps use the exact UTC Date.toISOString() representation.

The API treats route JSON as untrusted input until the shared verifiers produce branded verified values. The app applies the same discipline before persisting writer projections, accepting response key material, or decrypting content.