.uddp)Status: Draft
Date: 2026-07-14
Reference implementation: packages/decryptor (generateUddp), tests in
packages/decryptor/src/uddp.test.ts
Depends on: capsule-v1.md, tsp-v1.md (capsule wire
formats — the container packages a capsule, it does not redefine one)
The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.
A .uddp file is the distribution format of a sealed capsule: a single file the
recipient can keep for decades and open with no software from this project — no
web app, no CLI, no installed application. Everything needed travels inside:
capsule-v1.md / tsp-v1.md);OPEN-ME.html, all cryptography inlined);The design answer to “what if the program we made no longer exists?” is: a copy of the opener is inside every capsule file. The project dying cannot take the opener away from a recipient who holds the file.
A proprietary extension and platform-independent opening pull in opposite
directions: an OS with no registered handler opens .uddp with nothing. The
resolution is the .docx/.epub/.jar pattern — an own extension over plain
ZIP. ZIP has a 30+ year ubiquity record, a public specification, built-in readers
in every OS file manager, and unmistakable magic bytes (PK): any technician
confronted with an unknown file will identify and unpack it. The container’s job is
to make sure what they find inside explains itself.
The container is packaging, not protocol. The embedded capsule.json is the
sole normative artifact: the creator signature, the payload hash, and every
verification claim are defined over the capsule JSON exactly as in the wire-format
specs — never over the container or the opener. Consequences:
capsule.json and opening it with any conforming TSP opener MUST
produce the identical plaintext.capsule.json in a newer container changes nothing
normative, and MUST NOT modify its bytes.| Rung | Requires | Mechanism |
|---|---|---|
| A — one click | our application installed | app registers .uddp; double-click opens directly |
| B — two steps | any OS + any ES2022 browser, offline | open/rename as ZIP → README.txt says: open OPEN-ME.html in a browser |
| C — extract & open elsewhere | any conforming TSP opener | unzip, take capsule.json |
| D — re-implement | a competent engineer, a text editor | README.txt + spec/ + test vector: rebuild an opener from documents alone |
Rung A is the product; rungs B–D are the century insurance. A container that delivers A but not B–D is a demo, not a UDDP file.
.uddp file is a ZIP archive per the public ZIP APPNOTE specification.store (0) or deflate (8) only. No encryption,
no patented methods, no multi-volume. ZIP64 MAY be used only when an entry exceeds
4 GiB./, no .. segments..uddp. MIME type: application/x-uddp. A renamed file
MUST still open (the extension is a convention, not load-bearing).mimetype entry)Following the EPUB convention, the first entry in the archive MUST be named
mimetype, stored uncompressed (method 0), no extra field, containing exactly
the ASCII bytes:
application/x-uddp
This places the string at a fixed offset near the start of the file (local header
30 bytes + 8-byte name → content at offset 38), so file-style detectors identify a
UDDP container from the first ~60 bytes without unpacking: PK\x03\x04 at offset 0,
mimetype at offset 30, application/x-uddp at offset 38.
mimetype # REQUIRED, first, stored — §2.1
capsule.json # REQUIRED — the normative capsule document, byte-exact (§3)
OPEN-ME.html # REQUIRED — self-contained offline opener (§4)
README.txt # REQUIRED — plain-text instructions (§6)
spec/ # REQUIRED — the protocol documents (§6)
capsule-v1.md # (and/or tsp-v1.md — whichever governs this capsule)
vectors/… # at least one known-answer test vector
payload.bin # OPTIONAL — raw ciphertext bytes for large payloads (§7)
beacon.json # OPTIONAL — pre-fetched round-R beacon, if available (§5)
manifest.json # OPTIONAL — SHA-256 of every other entry (§8)
Unknown additional entries: readers MUST ignore entries they do not recognize
(forward compatibility of packaging) — but the opener and extractors MUST take the
capsule only from capsule.json, never from an unknown entry.
capsule.json (rung C — machine extraction)capsule.json per container.capsule.json. Nothing else.OPEN-ME.html (rung B — the embedded opener)A single self-contained HTML5/UTF-8 document that opens from file://:
crypto.subtle),
WebAssembly, TextEncoder/TextDecoder, Uint8Array, DOM, and the File API
(§7). SHOULD declare a <meta http-equiv="Content-Security-Policy"> forbidding
everything except inline script/style and connect-src to the documented beacon
endpoints.capsule.json (after
the mandatory </script> → <\/script> escaping, which is reversed before use),
so the HTML alone still opens the capsule if separated from the container.
capsule.json remains the authority; a mismatch between the two copies MUST fail
loudly if ever detected.spec/vectors/) and run it before first decryption; a failing self-test
blocks opening with an explanatory error — a silently miscomputing opener is
worse than none.README.txt and spec/.OPEN-ME.html embeds a compiled bundle that cannot be byte-identically rebuilt
across toolchains, so it is produced once as a versioned template artifact
(opener-template-v<N>.html, published with its SHA-256 — see RFC-0010 §3), and
every generator produces the opener by single-pass placeholder substitution:
the template is scanned for {{UNTILUM_[A-Z_]+}}; each match is replaced;
substituted values are never re-scanned. Generators MUST NOT modify template bytes
outside the placeholders.
| Placeholder | Replacement |
|---|---|
{{UNTILUM_TITLE}} |
human title, HTML-escaped |
{{UNTILUM_OPEN_DATE}} |
unlock.at (ISO-8601 UTC), HTML-escaped |
{{UNTILUM_ROUND}} |
decimal round number |
{{UNTILUM_CAPSULE_JSON}} |
capsule JSON, byte-exact, then </script> → <\/script> (case-insensitive) |
{{UNTILUM_CONFIG_JSON}} |
the runtime config JSON (below), same </script> escaping |
HTML-escaping: exactly &→&, <→<, >→>, "→", in that
order, nothing else.
The runtime config is serialized with JSON.stringify semantics (no added whitespace, UTF-8 verbatim, standard escapes) and this exact key order:
{"capsuleJson": "<capsule JSON as a string>",
"openDateIso": "<unlock.at>",
"round": <round>,
"beaconId": "<unlock.beacons[0].id>",
"embeddedBeacon": null | {"round": …, "signature": "…", "randomness": "…"}}
beacon.json (§5) uses the same three-field key order. With the same inputs and
the same template version, every conforming generator MUST produce a
byte-identical container (RFC-0010 §F2); README.txt rendering follows the
reference implementation byte-for-byte (packages/decryptor/src/uddp.ts).
The opener MUST offer, in order of preference:
beacon.json / embedded — when the container was generated (or re-issued)
after the unlock date, the round-R beacon MAY ride along; it is still
BLS-verified at open time (embedding bypasses the fetch, never the verification).https://api.drand.sh/<chainHash>/public/<round> and the Cloudflare
relay, as pinned constants.README.txt MUST explain how to obtain a historical round if all listed URLs are
dead (drand mirrors; the Arweave beacon archive when available).
README.txt and spec/ (rung D — re-implementation)README.txt is plain ASCII/UTF-8 text, no markup dependency, and MUST contain:
OPEN-ME.html in any browser”), stated simply enough for a
non-technical recipient.tlsecret, Argon2id
parameters, the version’s HKDF info strings — "untilum-v2" /
"untilum-v2-meta" for v2, "capsule-v1" / "capsule-v1-meta" for v1 —
AES-256-GCM layout) with all pinned quicknet constants, self-contained:
correct even if every project URL is dead.spec/ MUST contain the wire-format specification governing this capsule’s version
and at least one known-answer test vector, so a re-implementation can prove itself
before touching the real capsule. This is RFC-0009 §6 made literal: the documents
that guarantee resurrection ride inside every capsule file.
payload.bin)capsule.json per the wire format, and
OPEN-ME.html carries it in its embedded copy.payload.bin entry (stored, not deflated — ciphertext does not
compress), with capsule.json referencing it by hash. This avoids base64 (+33%)
and keeps the container fully autonomous. Because browsers do not allow
file:// pages to read sibling files, OPEN-ME.html MUST provide a file picker
(“select payload.bin from the extracted folder”) and MUST verify the selected
bytes against payload.hash before decrypting.payload.hash) MAY be packaged without payload.bin; the opener and
README.txt MUST say plainly that the bytes require storage liveness, and list
the txid plus several gateway alternatives. Fetched bytes MUST be verified
against payload.hash before decrypting.The creator signature covers the capsule JSON only (§1.2). The wrapper — opener,
README, specs — is not covered: a party who can modify the file could replace
OPEN-ME.html with one that exfiltrates the password. This is inherent to any
self-executing container. Mitigations, in force order:
.uddp file, so one transaction id immutably pins
capsule and wrapper together. A recipient who fetches by txid holds a container
nobody could alter after sealing.manifest.json
(SHA-256 of every other entry) lets a reader localize which entry was altered —
noting that a wrapper-tamperer can rewrite the manifest too; only the external
pin (1) or hash (3) is load-bearing.None of these makes tampering impossible for an attacker who controls both the file copy and the recipient’s trust; they make it detectable by anyone who checks.
manifest.json carries {"uddp": 1, …} when present;
otherwise the layout itself (§2.2) identifies v1. The capsule’s own v field
governs everything normative (§1.2).beacon.json after the unlock date. Re-wrapping MUST NOT modify
capsule.json bytes.A file is a conforming UDDP v1 container iff:
mimetype, stored, exactly application/x-uddp (§2.1).capsule.json, byte-exact as signed (§3).OPEN-ME.html: self-contained, offline-capable, embeds a byte-identical capsule
copy, BLS-verifies every beacon, runs the KAT self-test, leaks no secrets (§4).README.txt with the two-step instruction, algorithm summary, and constants;
spec/ with the governing spec and a test vector (§6).Implemented in packages/decryptor (generateUddp): the deterministic store-only
ZIP container with the mimetype convention (§2), byte-exact capsule.json (§3),
OPEN-ME.html with the extractable untilum-capsule block, CSP declaration, and
emergency appendix (§4), beacon.json embedding (§5), generated README.txt +
spec/ packaging (§6), inline-ceiling enforcement (§7), and manifest.json (§8.3).
Not yet implemented: the payload.bin + file-picker flow for large payloads (§7 —
the generator rejects ciphertext above the inline ceiling instead), the
by-reference variant (§7), and .uddp registration in the desktop application
(rung A of §1.3).
| Date | Change |
|---|---|
| 2026-07-14 | Initial version (as TDDP; renamed to UDDP 2026-07-16 with the project rename, same wire layout). |
| 2026-07-17 | Publication pass for the series Last Call: BCP 14 boilerplate. |