[spec]

The manifest specification, version 1.0

A production manifest is one document per delivery. It names every file by fingerprint and declares, element by element, how each was made: what was recorded in camera, what was rendered, and how the two were composited. The studio signs it with a key it controls and publishes the matching public key on its own domain.

It is a self-declaration. A valid signature proves the document has not changed since it was signed and that it was signed by the holder of a particular private key. It proves nothing about where any pixel came from. That limit is deliberate. It is the difference between this format and the ones that claim more than they can deliver.

The format exists because AI-disclosure law puts the duty on the advertiser. New York’s synthetic performer statute took effect in June 2026 and other states are drafting. A brand advertising in New York needs a record from its production vendor stating what in the deliverable is synthetic. Studios had no clean way to hand one over. Content credentials certify a session rather than a source. Platform toggles are binary and cannot describe a hybrid frame. Neither matches the actual shape of modern production, which is a recorded bottle standing in a rendered room.

The format is open. Any studio may implement it, publish its own key on its own domain, and issue manifests under its own name. There is no registry, no central authority, and no permission to ask. Verification needs a SHA-256 implementation and an Ed25519 library and nothing else.

What a manifest is, and is not

A valid signature proves two things and only two things: the document has not been altered since it was signed, and it was signed by the holder of a particular private key.

It does not prove that the declarations are true, that the studio is who it says it is, or where any pixel originated. Tying a key to a real studio happens out of band, by publishing the public key on the studio’s own domain and comparing fingerprints by hand.

A manifest describes the files it names, identified by fingerprint. A file that has been edited, re-encoded, or re-exported after delivery will no longer match. The manifest describes what left the studio.

What a manifest declares

Declarations are element-level. Each deliverable carries a list of elements, and each element has a role, an origin, and the tooling used.

Roles are subject, product, talent, environment, typography, audio, motion, and other.

Origin is recorded or rendered. There is no third value.

There are deliberately no percentages, no confidence scores, and no pixel or region maps. A manifest says which parts of a frame were recorded and which were rendered, and with what, and nothing finer. Precision beyond that would be invented rather than observed.

A deliverable’s classification is derived from its elements rather than stored: recorded when every element is recorded, rendered when every element is rendered, hybrid when they are mixed. A deliverable with no elements has no classification.

The document

{
  "id": "FML-2026-0729-01",       // {initials}-{yyyy}-{mmdd}-{seq}, studio-assigned
  "specVersion": "1.0",
  "issuedAt": "2026-07-29T15:00:00.000Z",   // ISO 8601
  "client": "Devil's Grin Texas Gin",
  "project": "elevated americana, spring campaign",
  "campaign": "spring 2026",       // optional
  "notes": "…",                    // optional
  "deliverables": [ /* Deliverable */ ],
  "signature": {
    "alg": "ed25519",
    "publicKey": "1e35db…",        // 32 bytes, lowercase hex
    "value": "ee9efe…",            // 64 bytes, lowercase hex
    "signedAt": "2026-07-29T15:04:12.000Z"
  }
}

Deliverable

{
  "filename": "DGG_spring_bottle_hero_4000x5000.jpg",
  "sha256": "05af49…",             // 32 bytes, lowercase hex, of the file's bytes
  "bytes": 3145728,
  "mediaType": "image/jpeg",
  "dimensions": "4000×5000",       // optional; video adds " · MM:SS"
  "elements": [ /* Element */ ],
  "compositing": "…",              // optional
  "finishing": "…",                // optional
  "syntheticPerformer": false      // the asset depicts an AI-generated human
}

Element

{
  "role": "product",               // subject | product | talent | environment
                                   // typography | audio | motion | other
  "origin": "recorded",            // recorded | rendered
  "tooling": "Hasselblad X1D II 50C",
  "notes": "studio, single strobe" // optional
}

Optional members are omitted when empty, never serialised as "" or null. This matters. The signature covers an exact byte sequence, so an empty string and an absent key are different documents.

Signature scheme

Given a manifest object M:

  1. Remove the signature member. Call the rest U.

  2. Serialise U as canonical JSON. Encode as UTF-8. Call these bytes C.

  3. D = SHA-256(C), 32 bytes.

  4. The signature is Ed25519-Sign(privateKey, D), hex-encoded, in signature.value.

Ed25519 signs D, the 32-byte digest, not C. A verifier that signs or verifies over C directly will not interoperate.

Canonical JSON

A total order on the serialisation, so that two implementations produce identical bytes.

Object members are sorted by key, ascending, by UTF-16 code unit, which is the order JavaScript’s default string comparison gives. Members whose value is undefined are omitted, while null is a value and is kept. There is no whitespace anywhere: no spaces after : or ,, no newlines, no trailing newline. Arrays keep their order, and array elements are canonicalised recursively. Strings, numbers, booleans and null are serialised as JSON.stringify would, with strings double-quoted and ", \ and control characters escaped, and integers written without a decimal point.

Sorting applies recursively at every level, including objects inside arrays.

Verifying a manifest

import * as ed from '@noble/ed25519'
import { createHash } from 'node:crypto'

function canonical(v) {
  if (v === null || typeof v !== 'object') return JSON.stringify(v)
  if (Array.isArray(v)) return `[${v.map(canonical).join(',')}]`
  return `{${Object.entries(v)
    .filter(([, x]) => x !== undefined)
    .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
    .map(([k, x]) => `${JSON.stringify(k)}:${canonical(x)}`)
    .join(',')}}`
}

export async function verify(manifest) {
  const { signature, ...unsigned } = manifest
  if (signature?.alg !== 'ed25519') return false
  const digest = createHash('sha256').update(canonical(unsigned), 'utf8').digest()
  return ed.verifyAsync(
    Buffer.from(signature.value, 'hex'),
    digest,
    Buffer.from(signature.publicKey, 'hex'),
  )
}

To check the delivered files as well, hash each one with SHA-256 and compare against the sha256 of the deliverable with that name. A name that matches with a different hash means the file changed after delivery. A hash that matches under a different name means the file was renamed, which the manifest does not prohibit.

Key publication

The signature verifies against the public key embedded in the manifest. That alone says nothing about whose key it is. To tie a key to a studio, the studio publishes it at https://{domain}/.well-known/manifest-key.json.

{
  "specVersion": "1.0",
  "studio": { "name": "…", "legalName": "…", "domain": "…",
              "contactEmail": "…", "location": "…" },
  "activeKey": {
    "alg": "ed25519",
    "publicKey": "1e35db…",
    "fingerprint": "2a730b5b80044d4f",
    "activeFrom": "2026-07-29T15:00:00.000Z"
  },
  "retiredKeys": [
    { "alg": "ed25519", "publicKey": "…", "fingerprint": "…",
      "activeFrom": "…", "activeUntil": "…" }
  ]
}

A verifier compares the fingerprint of the key embedded in the manifest against the fingerprints published on the domain. Retired keys are listed so that a manifest signed in 2026 still verifies after a 2027 rotation.

Publication is recommended, not required. A manifest verifies without it. What it lacks without publication is any link between the key and a named studio.

Ford Media Lab publishes its key at https://fordmedialab.com/.well-known/manifest-key.json.

Fingerprint

fingerprint = hex(SHA-256(publicKey bytes)[0..8])

The first 8 bytes of the SHA-256 of the 32 raw public key bytes, not of its hex text, lowercase hex, 16 characters. It is displayed in groups of four, as in 2a73 0b5b 8004 4d4f, purely to make hand comparison easier. The grouping is not part of the value.

Rotation

A rotation generates a new keypair, moves the old key into retiredKeys with an activeUntil, and discards the old private key. Public halves are kept permanently. Manifests already signed are never re-signed.

Versioning

This document specifies specVersion 1.0. A future version that changes the canonical form, the digest, the signature algorithm, or the meaning of any field will carry a different specVersion, and will be published at its own permanent address. Verifiers should refuse versions they do not implement rather than guessing.

Ford Media Lab authored this format and ships a manifest with every delivery. The format is open and unowned. Implement it, publish your key, and sign your own work.