﻿# CI-TDF Manifest Profile

Profile: `CI-TDF-1.0`

This document describes the manifest produced and consumed by this Delphi CI-TDF implementation,
developed for ClassifyIt.

## What CI-TDF Is

CI-TDF is a self-contained, cryptographically verifiable container format for sensitive documents. A
CI-TDF container carries its encrypted payload together with everything needed to authenticate,
verify, and — for an authorized recipient — decrypt it: recipient key-access entries, integrity
proofs at both the segment and whole-manifest level, and signed assertions about the payload's
classification and origin. None of that depends on the network, storage location, or application
that later opens the file.

It was built because the alternative was to wait: the documents that would authoritatively define a
NATO/FVEY-aligned container format (ACP-240 Supplements 3 and 4) are not publicly available, and the
labelling standards that are public (STANAG 4774/4778) define concepts and binding profiles but not
a ready-made JSON container. Rather than defer ClassifyIt's need for a working, verifiable container
until those gaps close, CI-TDF assembles a concrete format now — built on the open OpenTDF manifest
structure, informed by STANAG 4774/4778's concepts, and precise enough that every cryptographic
claim it makes can be independently checked (which this design process has actually done,
repeatedly, against real signed containers).

That independence from any single verifier is also the point, practically, for zero trust: a CI-TDF
container's confidentiality, integrity, and authenticity are all enforced by cryptography the
recipient (or, for the digital-signature-bound assertions, even a non-recipient policy-enforcement
point) can check directly — never by trusting the channel the file arrived through, the server that
stored it, or an operator's say-so. It works fully offline today, with local certificate- and
password-based key access; a federated `kas` mode is planned as an addition, not a replacement, so a
deployment can start simple and grow into centralized or federated key management without discarding
what it started with.

The current container is a ZIP package.

Entries:

- `0.manifest.json` — UTF-8 JSON manifest described here.

- `0.payload` — Compressed and encrypted payload bytes.

- `0.metadata` (optional) — Plaintext discovery/indexing metadata. See Metadata Fields. When absent from the container, the manifest's `metadata` object is also absent — the two are always in sync; there is no state where one exists without the other.

The reader also accepts legacy `manifest.json`, but new containers should write `0.manifest.json`.

## Relationship to Other Standards

This section covers what CI-TDF is not, and why — the positive rationale above should be read
alongside it, not instead of it.

CI-TDF is a project-local container format. It is built on the public **OpenTDF** manifest structure
(`github.com/opentdf/spec`) and informed by concepts from NATO **STANAG 4774** (confidentiality
metadata label syntax) and **STANAG 4778** (metadata binding mechanism, specifically the
digital-signature cryptographic artefact approach, per the publicly available ADatP-4778.2 companion
document).

CI-TDF is **not**:

- A conformant OpenTDF implementation. CI-TDF diverges from the published OpenTDF schema in several places — custom root-level fields (`c14nAlg`, `rootSignature`), a JWS-based assertion binding scheme distinct from OpenTDF's own `Sign`/`Verify` model, and a project-specific AAD construction. A tool built strictly against the OpenTDF schema should not be assumed to parse a CI-TDF manifest without modification.
- A conformant implementation of **ZTDF** or **ACP-240**. ACP-240 Supplements 3 and 4 — the documents that define ZTDF's specific requirements — are CCEB/FVEY publications that are not publicly available and have not been reviewed as part of this design. No claim of ZTDF or ACP-240 conformance is made or implied.
- A conformant implementation of the **STANAG 4774/4778** binding profiles. STANAG 4778's own companion SRD (ADatP-4778.2) currently defines cryptographic artefact binding profiles for XML Signature and CMS only; it does not yet define a profile for JSON/JWS-based binding. CI-TDF's assertion binding is informed by STANAG 4778's *choice* of mechanism (digital signature over a shared-secret HMAC, for the reasons discussed in the design log) but is not a defined STANAG 4778 profile.

Where this document says "aligned with" or "informed by" a standard, it means the design intent
follows that standard's concepts and rationale — not that the byte-level structure has been
validated against that standard's official schema or test vectors.

## Manifest Tree

```text
[Root JSON Object]
|-- tdf_spec_version
|-- objectId
|-- createdAt
|-- c14nAlg
|-- rootSignature
|   |-- alg
|   |-- kid    (sender signature only)
|   |-- x5c    (sender signature only)
|   `-- sig
|-- payload
|   |-- source
|   |   |-- fileName
|   |   |-- mimeType
|   |   `-- fileSize
|   |-- compressionAlg
|   `-- hash
|       |-- alg
|       `-- value
|-- metadata                 (optional)
|   |-- compressionAlg
|   `-- hash
|       |-- alg
|       `-- value
|-- encryptionInformation
|   |-- method
|   |   |-- algorithm
|   |   |-- iv
|   |   |-- ivDerivation
|   |   |-- keyDerivation
|   |   `-- contentEncryptionKeyId
|   |-- keyAccess[]
|   |   |-- accessMode
|   |   |-- wrappingAlg
|   |   |-- p2s                 (password access only)
|   |   |-- p2c                 (password access only)
|   |   |-- url                 (certificate access)
|   |   |-- kid                 (certificate access)
|   |   |-- ephemeralPublicKey  (ECC certificate access only)
|   |   `-- wrappedKey
|   |-- policy               (planned, see Roadmap)
|   `-- segmentIntegrity
|       |-- aad
|       |   |-- profile
|       |   `-- fields[]
|       `-- segments[]
|           |-- tag
|           `-- encryptedSegmentSize
`-- assertions
    `-- iCLASS
        |-- org
        |-- class
        |-- rel[]      (optional)
        `-- cat[]      (optional)
```

`assertions` is a JSON object keyed by assertion name, not an array — currently one entry, `iCLASS`.
No `id` field inside its content — the object key is the id. `iCLASS` is a bare object, protected
only by `rootSignature` (whole-manifest coverage) — see the note below on why an `iCLASS`-level
signature was considered and rejected. A `data-provenance` assertion previously existed here; it was
removed — see Roadmap for when it would come back.

## Supported Crypto

Content encryption:

- `AES-256-GCM` — Payload encryption. The DEK is 32 bytes. Each encrypted segment stores ciphertext followed by a 16-byte GCM tag.

- `CI-TDF-segment-aad-v1` — Current AES-GCM AAD profile. The implementation authenticates a canonical JSON AAD object containing `aadProfile`, `objectId`, and `payloadUrl`.

- `HKDF-SHA256` — Used for internal derived keys.

Key access modes:

- `cert` / `RSA-OAEP-256` — Wraps the DEK for an RSA recipient certificate. Uses RSA-OAEP with SHA-256 and MGF1-SHA-256.

- `cert` / `ECDH-ES+A256KW` — Wraps the DEK for an ECC recipient certificate. Uses ephemeral ECDH, a SHA-256-derived KEK, and AES-256 key wrap.

- `pass` / `PBES2-HS256+A256KW` — Wraps the DEK from a user password. Uses PBKDF2-HMAC-SHA256 and AES-256 key wrap.

`cert` and `pass` are both local, offline key-access modes with no network dependency. A third mode,
`kas` (remote Key Access Server, OpenTDF-style), is planned — see Roadmap. `cert` and `pass` will
remain supported alongside `kas` once added; they are not an interim substitute for it.

## Root Fields

### `tdf_spec_version`

Purpose: The single, authoritative identifier of both the format family and its version. This is the
standard OpenTDF field name (`opentdf.io/spec/schema/opentdf/manifest` uses it for a comparable
purpose), repurposed here to carry a CI-TDF-specific, non-numeric-vs-OpenTDF value — deliberately
not a value that could be misread as an OpenTDF release number. There is no separate profile/version
field; this is the only one.

Type: string

Value domain: `CI-TDF-<major>.<minor>`

Current value: `CI-TDF-1.0`

Status: implemented

### `objectId`

Purpose: Stable identifier for the Trusted Data Object. Not just descriptive — this exact string is
embedded verbatim in the AAD JSON used to authenticate every AES-GCM segment, so its serialization
must be fixed and mandatory, not merely "a UUID."

Type: string

Value domain: RFC 4122 UUID v4, canonical hyphenated form (`8-4-4-4-12`), **uppercase** hex, no
enclosing braces, no `urn:uuid:` or other scheme prefix. Example:
`1B016210-D709-4FDC-9DDB-E3578A7C1108`.

Rule: this exact string — case included — is used byte-for-byte wherever `objectId` appears in AAD
construction. No case normalization or reformatting is permitted between manifest write and AAD
build.

Used by:

- AES-GCM AAD

Status: implemented; value-domain rule above is a tightening of the previous "UUID string"
definition

### `createdAt`

Purpose: Container creation time. Kept deliberately as a simple root field rather than following
OpenTDF's approach of bundling creation time into an optional, independently-signable
`system-metadata` assertion (alongside SDK version, OS, architecture) — that independence isn't
needed here, since `createdAt` is already covered by `rootSignature`'s whole-manifest
canonicalization, and a bare root field is simpler. Retained (rather than removed) because it may
support a future use, e.g. an audit/debug assertion, without needing to be reconstructed later.

Type: string

Value domain: ISO 8601, UTC only, whole-second precision, always `Z` suffix — exactly
`YYYY-MM-DDTHH:MM:SSZ`. No fractional seconds. No other timezone offsets (e.g. `+00:00`) permitted;
`Z` is the only legal representation of UTC here.

Status: implemented. Informative only — not checked by any validation rule (e.g. no rejection for
clock skew or future-dated containers).

### `c14nAlg`

Purpose: Declares the single, global deterministic-serialization algorithm used to produce canonical
bytes wherever this manifest needs something to hash or sign — `rootSignature` (whole manifest),
`aad` (the small AAD object), and every assertion `binding` (that assertion alone). One declaration,
implicitly consumed by all three; no per-scope restatement.

Named after the established XML-DSIG numeronym for "canonicalization" (`C14N`), matching the
terminology used by STANAG 4778's own Digital Signature binding profile (XML Signature /
`CanonicalizationMethod`).

A flat root string, not an object. Value domain: enum, currently one legal value: `JCS-RFC8785`.
Declared as an enum (not a bare constant) so a future v2 canonicalization can be added without
changing the field's meaning.

Status: implemented. Verified against an independent RFC 8785 canonicalizer for idempotency
(re-canonicalizing this implementation's output reproduces it byte-for-byte) and against real signed
manifests (ES256/ES512/RS512 `rootSignature` and assertion JWS signatures verified independently).

### `rootSignature`

Purpose: Manifest-level integrity or digital signature, covering the entire canonicalized manifest,
including `assertions` and `payload`.

Naming note: `rootSignature` is OpenTDF's own field name for this concept (their
`IntegrityInformation.RootSignature`, and their tamper errors
`ErrRootSigValidation`/`ErrRootSignatureFailure`), retained deliberately to signal real lineage.
Scope differs, though: OpenTDF's `rootSignature` is an HMAC over the *concatenation of segment
hashes* (ciphertext integrity only). This field's scope is broader — it covers the entire
canonicalized JSON manifest, including `assertions` and `payload`. Same name, same conceptual role
("the one signature that covers everything"), larger scope — not byte-for-byte equivalent semantics.

This field answers a question independent of `keyAccess[].accessMode`: `accessMode` governs how each
*recipient* unwraps the DEK (a confidentiality concern, decided per recipient). `rootSignature.alg`
governs how the *sender* authenticates the whole manifest (an integrity/authenticity concern,
decided once, depending only on whether the sender has a signing identity loaded) — the two are
independent axes. A manifest can deliver keys via password only and still be asymmetrically signed,
or deliver keys via certificate and still fall back to `HS256`.

Fields:

- `alg` — `HS256` — no persistent sender signing identity available. `ES256`, `ES384`, `ES512`, `RS256`, `RS384`, or `RS512` — persistent sender signing identity available.

- `sig` — Base64 HMAC or signature bytes.

- `kid` — Sender certificate thumbprint (`SHA-256` of the full DER `x5c` certificate). Present only in sender-signature mode. Kept even though it's derivable from `x5c` — gives a verifier a cheap string-comparison identity check (e.g. "does this signer match a `keyAccess[].kid`?") without decoding and hashing a certificate first.

- `x5c` — Sender signing certificate — "X.509 Certificate Chain," the same header parameter name and purpose as JOSE/JWS's `x5c` (RFC 7515 §4.1.6). Currently stores a single certificate, not the full chain — see Future work. Present only in sender-signature mode.

### `HS256` vs. asymmetric mode — what each actually protects, and why there is no third option

The choice of `alg` here has a real, disclosed security trade-off — not just an implementation
detail.

**`HS256` (no persistent sender identity):** keyed by DEK-derived material. This means **only a
recipient who can already unwrap the DEK can verify `rootSignature`** — a policy-enforcement point
or any party without decrypt access cannot verify manifest integrity at all in this mode. This is a
real, accepted limitation for ad-hoc, password-only usage, not an oversight — it should be stated
plainly in any deployment that might route containers through infrastructure needing to verify
without decrypting, rather than silently relied upon.

Exact `HS256` recipe:

1. Build the root-signature input by parsing the manifest JSON, removing only `rootSignature.sig`, and canonicalizing the resulting JSON with the manifest's `c14nAlg` (`JCS-RFC8785`). The `rootSignature` object itself remains present, including `alg`.
2. Derive the 32-byte root-signature HMAC key with HKDF-SHA256 using `IKM = DEK`, `salt = baseIV`, `info = ASCII "-2"`, and `L = 32`.
3. Compute `HMAC-SHA256(rootSignatureKey, canonicalManifestBytes)`.
4. Store the raw 32-byte HMAC as standard Base64 without embedded line breaks in `rootSignature.sig`.

**Asymmetric mode (`ES*`/`RS*`) requires a *persistent* signing identity — CA-issued (preferred) or
self-signed-but-reused across every container from that sender.** A fresh, ephemeral self-signed
certificate generated per container was considered and explicitly rejected: it produces a
`rootSignature.sig` that validates cleanly, but with no way for a verifier to distinguish "signed by
the legitimate sender's throwaway key" from "signed by an attacker's throwaway key" after tampering
with the manifest and re-signing. That's worse than `HS256`'s honest "cannot verify" — it's a false
"verified," which looks like a security guarantee while providing none. A **persistent** identity,
by contrast, supports trust-on-first-use (TOFU, the same model SSH host keys rely on): a verifier
that has seen this sender's `kid` before can detect the moment it unexpectedly changes, even without
a CA relationship. This does not protect the very first container a verifier ever sees from that
sender (no design without an out-of-band identity check can), but it makes every subsequent
container checkable and makes key substitution a visible signal rather than something silently
accepted.

No ephemeral-per-container mode exists in this format — it was evaluated and rejected for the reason
above, not merely left unimplemented.

Status: implemented and verified end-to-end for asymmetric mode. Both RS512/RSA-4096 and ES512/P-521
signed manifests have been independently canonicalized (using `c14nAlg`) and cryptographically
verified against the embedded `x5c` certificate, confirming the canonicalization and signing
pipeline agree byte-for-byte. `HS256`/DEK-derived mode confirmed implemented as described above; the
PEP-unverifiable limitation is disclosed, not yet mitigated (mitigation would require
persistent-identity signing to be mandatory rather than a fallback — a deployment-policy decision,
not a schema change).

Future work:

- make `x5c` a certificate-chain array
- include local certificate validation metadata
- record local CA validation outcome in the manifest
- add a binding-creation timestamp distinct from `createdAt`, to support replay detection and to record that the signing key was valid at signing time even if since expired or revoked
- consider whether persistent-identity signing should become mandatory (rather than an `HS256` fallback) for deployments where PEP verification without decrypt access is a requirement

## Payload Fields

### `payload.source`

Purpose: Groups the facts that describe the *original* file, before any transform — as opposed to
facts about the stored, compressed-and-encrypted blob, which live at `payload`'s own level. This
distinction is carried structurally (which object a field sits under), not by naming or a `scope`
field.

Fields:

- `fileName` — Purpose: Original plaintext filename. Metadata only — must not contain a local path. Mandatory rule: no path separators (`/`, `\`), no drive letters, no `..` sequences. A filename that reaches an extraction routine unchecked is a path-traversal vector.

- `mimeType` — Purpose: MIME type of the original plaintext content.

- `fileSize` — Purpose: Original plaintext size, before compression and encryption. Type: integer.

Status: implemented

### `payload.compressionAlg`

Purpose: Declares the compression applied to the original plaintext before encryption.

Value domain: enum. `none` (no compression) or `zlib`; additional algorithms may be added.

Current value: `zlib`

Status: implemented, `none` and `zlib` supported

### `payload.hash`

Purpose: Hashes the stored, encrypted blob (`0.payload`'s raw bytes, including GCM tags) — not the
original file.

Fields:

- `alg` — Value domain: enum. Current value: `SHA-256`.

- `value` — Base64 digest of the raw encrypted `0.payload` bytes.

Status: implemented

## Encryption Information

### `encryptionInformation.method.algorithm`

Purpose: Content encryption algorithm. The sole authoritative declaration of this fact in the
manifest.

Value domain: enum. Current value: `AES-256-GCM`. Additional AEAD algorithms may be added.

Status: implemented

### `encryptionInformation.method.iv`

Purpose: Base IV for segment IV derivation.

Type: Base64 12 bytes

Status: implemented

### `encryptionInformation.method.ivDerivation`

Purpose: Describes segment IV derivation.

Current value: `sha256-base-iv-plus-segment-counter`

Exact derivation:

`segmentIV = SHA-256(baseIV || int32_le(segmentIndex))[0..11]`

where `segmentIndex` starts at `0` and is encoded as a 4-byte little-endian signed integer.

Status: implemented and verified — decryption using this exact derivation against real test
containers succeeded.

### `encryptionInformation.method.keyDerivation`

Purpose: Describes internal derived-key algorithm.

Current value: `HKDF-SHA256`

Status: implemented

### `encryptionInformation.method.contentEncryptionKeyId`

Purpose: Non-secret content key identifier.

Current derivation: `HMAC-SHA256(baseIV, DEK)`

Status: implemented and verified — independently recomputed and matched against real test
containers, confirming this is a reliable way to check a candidate DEK without attempting a full
decrypt.

### `encryptionInformation.method.aadProfile`

Purpose: Method-level declaration of AAD construction.

Suggested value: `CI-TDF-segment-aad-v2` or profile-defined value.

Status: missing. Current AAD profile is declared under payload transforms and integrity information.

## Key Access Fields

All current key-access entries have `type = "wrapped"`.

The `wrappedKey` is never plaintext DEK.

### `keyAccess[].accessMode`

Purpose: Selects the unwrap flow. This is the single source of truth for which access mode a
`keyAccess` entry uses — no separate `type`/`protocol` fields exist, since both were always fully
derivable from this one (`cert`/`pass` → local, wrapped delivery; `kas` → remote), never carrying
independent information.

Current values:

- `cert`
- `pass`

Planned:

- `kas` — remote Key Access Server, policy-gated key release. See Roadmap.

Status: `cert` and `pass` implemented for local certificate/password modes. `kas` planned.

### `keyAccess[].wrappingAlg`

Purpose: Identifies the DEK wrapping algorithm.

Current values:

- `RSA-OAEP-256`
- `ECDH-ES+A256KW`
- `PBES2-HS256+A256KW`

Status: implemented for local certificate/password modes

### `keyAccess[].url`

Purpose: Locates the key source. What this means, and how authoritative it is, differs by
`accessMode` — this field is not a single fixed protocol identifier, it takes on a different role
per mode:

- **`cert` mode**: `urn:file:<certificate-filename>` — the certificate's basename only, never a full local path. This is a **hint for a human or a local file-picker**, not a cryptographic check. The only authoritative identity check is `kid` matching the resolving certificate's SHA-256 thumbprint. A correct certificate that has been renamed or relocated must still be accepted on `kid` match alone — `url` should never cause a rejection by itself.
- **`pass` mode**: not applicable — password mode has no external key source to locate; omitted.
- **`kas` mode (planned)**: `url` becomes genuinely authoritative — the actual KAS endpoint (e.g. an HTTPS URI) a client must contact to request the key. Unlike `cert` mode, this is not a hint; it is the address the unwrap protocol depends on.

Status: implemented for `cert` mode as a non-authoritative hint. `kas` mode planned, where the
field's role changes to authoritative.

### `keyAccess[].kid`

Purpose: Identifies the wrapping key.

Certificate mode: SHA-256 thumbprint of the full DER X.509 certificate.

Not valid meanings:

- email address
- common name
- filename
- public-key-only hash, unless the final profile explicitly changes this

Status: implemented for certificates

### `keyAccess[].ephemeralPublicKey`

Purpose: Carries the ephemeral public key for ECC ECDH unwrap.

Required for: `cert` / `ECDH-ES+A256KW`

Omitted for:

- RSA certificate mode
- password mode

Status: implemented

### `keyAccess[].p2s`

Purpose: PBKDF2 salt.

Required for: password mode

Type: Base64 16 bytes

Status: implemented

### `keyAccess[].p2c`

Purpose: PBKDF2 iteration count.

Current value: `100000`

Required for: password mode

Omitted for: certificate mode

Status: implemented, should become policy/configurable

### `keyAccess[].wrappedKey`

Purpose: Recipient/password-specific encrypted DEK.

Type: Base64

Status: implemented

Not present: `policyBinding`. In the current `cert`/`pass`-only architecture it would be fully
redundant with `rootSignature`, which already covers the entire manifest (every `wrappedKey` and
every assertion together) in one signature — any tampering breaks `rootSignature.sig` regardless.
`policyBinding` becomes necessary once `kas` mode exists: a KAS evaluates policy and rewraps a key
dynamically, after the original signing event and by a different party than the sender — something
`rootSignature` structurally cannot vouch for, since it was computed once, at creation. See Roadmap.

## Segment Integrity

Purpose of this whole object: encryption-side integrity only — segment tags, segment sizing, and AAD
construction. Scoped to `encryptionInformation` because none of it means anything outside the
context of how the payload was AES-GCM-encrypted.

### `encryptionInformation.segmentIntegrity.aad`

Purpose: Describes AES-GCM AAD.

Current fields:

- `profile` — Current value: `CI-TDF-segment-aad-v1`

- `fields` — Current value: `["aadProfile", "objectId", "payloadUrl"]`

AAD canonicalization uses the manifest's single global `c14nAlg` declaration; no separate
canonicalization field exists at this scope.

Exact AAD object before canonicalization (per root `c14nAlg`):

```json
{"aadProfile":"CI-TDF-segment-aad-v1","objectId":"<objectId>","payloadUrl":"0.payload"}
```

Status: implemented and verified — `fields` now matches the actual AAD construction exactly;
decryption using this exact construction against real test containers succeeded.

Planned AAD fields (v2):

- `segment.index`
- `encryptedSegmentSize`
- `policyDigest`
- `contentEncryptionKeyId`

### `encryptionInformation.segmentIntegrity.segments[]`

Purpose: Per-segment integrity metadata.

Current fields:

- `tag` — AES-GCM authentication tag for this segment.

- `encryptedSegmentSize` — Ciphertext length for this segment, including the tag. The pre-encryption plaintext/compressed length is not separately stated — AES-GCM never pads, so it is always exactly `encryptedSegmentSize` minus the tag length for the algorithm named in `encryptionInformation.method.algorithm` (16 bytes for AES-256-GCM). A reader deriving this value needs an algorithm-to-tag-length mapping; the two other AEAD algorithms currently anticipated (ChaCha20-Poly1305, AES-256-GCM-SIV) also use 16-byte tags.

Status: implemented and verified — segment tag matches the trailing 16 bytes of `0.payload` exactly
on real test containers.

Future fields:

- `index` — Explicit segment index.

- `hash` — Optional object `{alg, value}` for a SHA hash of the segment's ciphertext, separate from the `tag` field.

## Assertions

The implementation currently writes one assertion type: `iCLASS`.

A `data-provenance` assertion (an `originator` field, self-asserted) previously existed. It was
removed: `originator` never carried more reliable information than what `rootSignature.x5c`/`kid`
already prove cryptographically about who signed the manifest, and the field had sat hardcoded,
never reflecting a real value. Reintroduce specifically when signer identity and content-originator
identity can genuinely differ — e.g. an automated pipeline signing on behalf of a human author, or a
system relaying a document originated by someone else — not before. See Roadmap.

## iCLASS Assertion

Purpose: A compact, human-and-machine-readable classification statement — this project's own iCLASS
design (see classifyit.eu case study). Replaces the earlier STANAG-4774-shaped `classification`
assertion entirely.

Keyed as `iCLASS` in the `assertions` object. A bare object — no `id`, `type`, `scope`,
`appliesToState`, `statement`, or `binding` wrapper, and no signature of its own — protected only by
`rootSignature` (whole-manifest coverage).

An `iCLASS`-level signature (`objectId` + a freshly-computed `payloadHash` +
`org`/`class`/`rel`/`cat`, signed and stored as `sig`) was designed and then deliberately rejected,
not simply never considered. It closed a real splicing risk (lifting a validly-signed `iCLASS` block
from one container into another with a different, undisclosed classification) — but the version
secure enough to actually close that risk requires recomputing `payloadHash` from the real
`0.payload` bytes on every check, which for a large file costs more than `rootSignature`
verification itself (JSON canonicalization vs. hashing the entire ciphertext). That defeats the
original motivation — a cheap check for infrastructure that can't afford full manifest verification
— and since manifest and payload are always bundled in the same container, there's no deployment
shape where `iCLASS.sig` would be reachable without `rootSignature` also being reachable and
cheaper. Revisit only if a real deployment shape emerges where an `iCLASS`-only artifact is
separated from its container.

### `org`

Purpose: Originating organization/nation short code. The current generator exposes this as the
editable iCLASS originator field.

Example: `BEL`

Status: implemented

### `class`

Purpose: Classification level, iCLASS single-letter code.

Value domain: `P` / `O` / `R` / `C` / `S` / `T`

Status: implemented. Mandatory — an assertion with no classification level says nothing.

### `rel`

Purpose: Releasability codes.

Type: array of short codes. Optional.

Status: implemented

### `cat`

Purpose: Category/compartment codes describing the type of data in the payload.

Type: array of short codes, flat (no restrictive/permissive typing). Optional.

Status: implemented

## Assertion Binding (currently unused, kept as documented, reusable infrastructure)

`iCLASS` does not use this mechanism — it is a bare object, protected only by `rootSignature`. This
section's mechanism is real, built, and independently verified (JWS signatures reconstructed and
cryptographically checked against the embedded `x5c` certificate on real test containers, ES256 and
ES512) — documented here so it isn't rediscovered from scratch if a future assertion needs
independent verifiability (e.g. a reintroduced `data-provenance`, per the condition above, or `Core
Metadata`).

### `binding.method`

Values:

- `none` — No sender identity was loaded. Assertion is root-integrity protected but not individually signed.

- `jws` — Sender identity was loaded. Assertion has a compact JWS-like signature.

Known inefficiency, if reintroduced: when an assertion signer is the same identity as
`rootSignature` (the common case), the JWS header re-embeds the full `x5c` certificate chain a
second (or third) time, purely redundantly — same `kid`, same certificate bytes. Resolve `kid`
against `rootSignature.x5c` or a manifest-level certificate table instead of re-embedding `x5c`,
while keeping `kid` itself (cheap, and needed if an assertion is ever signed by a different identity
than the root).

### `binding.profile`

Value when signed: `ADatP-4778` (informed by; not a conformance claim)

### `binding.signature`

Purpose: Compact JWS-like signature.

### `binding.digest`

Purpose: Digest of the bound statement/reference in addition to signature. Never implemented.

### `binding.signer`

Purpose: Assertion-level signer metadata (`kid`/`x5c`). Never implemented as a separate field — was
embedded directly in the JWS header instead; see the redundancy note above.

## Core Metadata (planned)

Purpose: Provide discovery and lifecycle metadata for the trusted data object, informed by STANAG
5636 concepts.

Expected assertion ID: `ci-tdf-core-metadata`

Expected fields may include:

- `title`
- `description`
- `subject[]`
- `creator`
- `publisher`
- `contributor[]`
- `date`
- `type`
- `format`
- `identifier`
- `source`
- `language`
- `relation[]`
- `coverage`
- `rights`
- lifecycle or operational-currency metadata

Status: missing

## Metadata Fields

Purpose: `0.metadata` is a plaintext (never encrypted) entry in the ZIP container, holding
information intended to support discovery and indexing — enough to identify or locate content,
deliberately not enough to convey sensitive content or full context. Because it is unencrypted, it
is verifiable by any party from `hash` and `rootSignature` alone, without a DEK or recipient
credentials — e.g. a search or indexing system can confirm a `0.metadata` entry hasn't been tampered
with, without ever gaining decrypt access. The current generator stores the selected metadata file
byte-for-byte as clear text in `0.metadata`; see `metadata.compressionAlg`.

Exactly one `metadata` entry, `0.metadata`, is currently supported, and it is optional — a container
may validly contain only `0.manifest.json` and `0.payload`. When a metadata file is selected in the
generator, the manifest's `metadata` object is emitted and the `0.metadata` ZIP entry is added. See
Roadmap for multiple metadata entries.

Open question, not yet resolved: the planned "Core Metadata" assertion (STANAG-5636-informed
discovery/lifecycle fields — `title`, `creator`, `subject`, etc.) and this `0.metadata` file serve
overlapping purposes — both are about findability rather than content. Worth deciding whether that
discovery content lives inside `0.metadata` itself, as an inline assertion, or both, rather than
building two separate discovery mechanisms.

### `metadata.compressionAlg`

Purpose: Declares the compression applied to the metadata content.

Value domain: enum. `none` or `zlib`; additional algorithms may be added.

Current value: `none`

Status: implemented for cleartext `none`; `zlib` is reserved for future use

### `metadata.hash`

Purpose: Hashes the stored metadata blob's plaintext bytes directly — no encryption to account for,
unlike `payload.hash`.

Fields:

- `alg` — Value domain: enum, same domain as `payload.hash.alg`. Current value: `SHA-256`.

- `value` — Base64 digest of the raw `0.metadata` bytes.

Status: implemented

## Validation Rules

A reader should reject a container if any of these checks fail:

1. The selected `keyAccess` entry cannot unwrap a 32-byte DEK.
2. `rootSignature.sig` does not validate over canonical manifest JSON with `rootSignature.sig` removed.
3. Any segment GCM tag in `segments[].tag` does not match the tag appended to `0.payload`.
4. AES-GCM decrypt fails using the derived segment IV, DEK, and AAD.
5. Payload decompression fails after successful decrypt.
6. The decompressed payload's length does not equal `payload.source.fileSize`.
7. The manifest's `metadata` object and the container's `0.metadata` ZIP entry disagree on presence — one exists without the other.
8. When `0.metadata` is present, its declared `metadata.hash` does not match the stored `0.metadata` bytes.

These validation rules have been exercised against real test containers during development; rules 2
and 4 were independently re-verified from outside the Delphi implementation (Python, using an
independent RFC 8785 canonicalizer and standard ECDSA/RSA verification), not just self-tested. Rule
6 reflects a check already relied on in that verification (decompressed length matched the
manifest's declared size exactly) that had not previously been written down as a formal rule.

An additional dormant rule — any `binding.method = "jws"` assertion failing payload or signature
verification — applied when assertions carried independent bindings (see Assertion Binding). No
current assertion does; the rule is dormant, not deleted, since it would apply again immediately if
a future assertion reintroduces `binding`.

## Roadmap

- Multiple payload and metadata entries per container — e.g. `0.payload`, `1.payload`, `2.payload` alongside `0.metadata`, `1.metadata`, under a single container. Under consideration, not yet designed in full. Key open decisions already identified: a single shared DEK across all payload entries in a container (current direction — simpler, and implies one classification decision covers the whole container, since anything decryptable by one recipient is decryptable for all payload entries under a shared DEK) versus per-payload DEKs (needed only if differently-classified payloads must coexist in one container, which is a materially larger change touching `keyAccess[]` and assertion `scope`); `payload`/`metadata` becoming arrays with a stable per-entry `id` independent of filename position; and whether an explicit relationship needs to be recorded between a given metadata entry and the payload entry it describes.
- `kas` access mode — remote Key Access Server, policy-gated key release, alongside the existing `cert`/`pass` local modes (not a replacement for them). Requires reintroducing `keyAccess[].policyBinding` (removed for now — redundant with `rootSignature` in the `cert`/`pass`-only architecture), since a KAS rewraps keys dynamically, after the original signing event and by a different party than the sender, which `rootSignature` cannot vouch for.
- `encryptionInformation.policy` — machine-readable ABAC-style access-control policy object, most likely required once `kas` mode exists.
- `encryptionInformation.policy.binding` — standalone cryptographic binding for the policy object.
- Richer AAD fields (v2): segment index, segment size, encrypted segment size, policy digest, content key ID.
- Explicit segment metadata: `index`, optional ciphertext SHA `hash` object separate from `tag`.
- Sender certificate chain: `x5c` as an array of certificate-chain entries rather than one string.
- Trim redundant per-assertion `x5c` embedding (see Assertion Binding note above).
- `iCLASS.cat` is currently a flat array with no typing (no restrictive/permissive distinction). Revisit only if flat proves insufficient for real access-control logic — the typed `tagName`/`type`/`values` structure originally designed for the STANAG-shaped `classification` assertion was dropped along with it, not carried into `iCLASS`.
- Reintroduce a `data-provenance` assertion specifically when signer identity and content-originator identity can genuinely differ (e.g. an automated pipeline signing on behalf of a human author, or a relay handling a document originated elsewhere) — not before.
- Trust validation metadata: local issuer CA, validation time, and EKU decisions recorded in the manifest.
- Reintroduce assertion `binding` (JWS mechanism, documented and verified but currently unused) if any future assertion needs independent verifiability apart from `rootSignature` — see Assertion Binding.
- Full STANAG-4774-informed fields (`ownerProducer`, `disseminationControls`, `displayMarking`, `policyReference`) — dropped along with the `classification` assertion in favor of `iCLASS`; revisit only if `iCLASS`'s minimal shape proves insufficient.
- Core metadata assertion: discovery, lifecycle, creator, publisher, identifier, and related fields.
- Comprehensive metadata reference: optional `0.metadata` entry with hash and binding.
- Full provenance: `authority`, `timestamp`, `tool`, `action`.
- Object-level schema metadata: `schema`, `schemaVersion`, `issuer`, `creator`, or `authority`.
- Binding-creation timestamp on `rootSignature` and each assertion `binding`, distinct from `createdAt`.
- Decide whether `HS256`/no-sender-identity mode should be disallowed for containers that may need verification without decrypt access.

Processing rules still needed:

- Certificate-chain serialization in `x5c`.
- EKU checks.
- Policy evaluation against authenticated user/entity attributes.
- Controlled-vocabulary normalization for labels and releasability.
- Audit evidence for validation and unwrap decisions.

## Implementation Limits

- This document describes the current Delphi implementation and profile direction for CI-TDF.
- It is not a formal OpenTDF, ZTDF, ACP-240, or NATO STANAG conformance statement — see "Relationship to Other Standards" above.
- Local CA signature validation, certificate validity-date checks, and key-usage checks are implemented for local certificate files.
- Multi-hop chain construction and manifest serialization of the certificate chain are not yet implemented.
- `x5c` currently stores one Base64 DER certificate string, not a JSON array of a chain, and is currently duplicated across `rootSignature` and each signed assertion even when the signer identity is the same.
- AAD currently covers `aadProfile`, `objectId`, and `payloadUrl`.
- Core metadata (STANAG-5636-informed) is not yet implemented.
- Single optional cleartext `0.metadata` entries are emitted byte-for-byte and hash-validated. Richer comprehensive metadata references are not yet implemented.
- Only `cert` and `pass` key-access modes exist; `kas` is planned, not a design dead-end.
