WAI Extension: Data Export
Mirrored from the canonical text at commit 0d3f0d7c ().
Status: Draft. Registers the record
wai.data.export: an account’s records as one bundle, signed whole by the service that held them, so the account’s holder can show later what it was handed: that no record was left out of it, changed in it or added to it, and in which order. A bundle (§3) is JSON Lines: a header naming the account, the part of it or none, the period and when the bundle was made; one record line for each record, naming it by its kind from receipt-ledger §5.2’s one registry of record kinds and by its identity, and carrying it as its issuer wrote it, its text for a JSON record and its bytes for a binary one; and a trailer: the number of records, the header’s digest, a digest over every record line’s in order, and the service’s Ed25519 signature. A bundle carries the account’s audit chain (audit-chain): its entries are record lines, its anchor is in the header and its checkpoint in the trailer, and a verifier holds them to the chain’s rules (§4). A record of a hosted service’s own, which WAI does not define, is not in a bundle (§5). Each record still verifies under its own text; a verifier of a bundle checks the bundle, every line’s form, kind and digest, and the chain it carries (§6). The reference implementation is thedata_exportmodule ofwai-rs(featuredata_export), andwai verifyreads a bundle by its name; the corpus islive-conformance/data-export/, and its second implementation islive-conformance/data_export_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.Texts read for this revision: the WAI texts this one cites, among them the bulk exports WAI already has, each of one kind and none signed whole (http-delivery §8.2’s receipt index and fleet §7’s bundle); RFC 8259; RFC 8785; RFC 8032; FIPS 180-4.
1. Scope
In scope: the bundle, its header, its record lines and its trailer, its signature and its digests (§3); the audit chain it carries (§4); what it does not carry (§5); and what a verifier checks of it (§6).
Out of scope: which records of an account a service puts in a bundle, and when; how a bundle is asked for, made or delivered; what each record means, which its own text states and its own verifier checks; and accounts, plans and keys, which are the service’s. A bundle names a part of an account (a workspace) as a token, and what a part is, is the service’s.
Relationship to the core spec: wai.data.export is a record (SPEC §5): a
record a WAI tool emits and verifies. It names no payload format and is not dispatchable.
2. Terms and encodings
- Document, canonical form, digest, token, key and signature: as route-record §2. Hash, zeros, instant, account: as audit-chain §2.
- Lines: a text of JSON Lines as audit-chain §6 states them: each line a document’s canonical form followed by one newline, at most 2²⁴ bytes before it, and nothing else.
- Record kind: an integer of receipt-ledger §5.2’s registry, from 1 to 127. Kinds 128 to 255 are private, and a WAI record never states one (receipt-ledger §5.2).
- Identity: what receipt-ledger §5.2’s row for a record’s kind names it by (a digest, a link hash, a receipt’s id), 32 bytes, as a hash.
- Service: the party that held the account’s records and signs the bundle.
3. The bundle
A bundle is lines: the header (§3.1), then one record line for each record (§3.2), then the
trailer (§3.3). A file that holds one is named with the suffix .wai-export.jsonl. Since every
line is its document’s canonical form, a bundle has one text, and each record line one digest.
3.1 The header
A document of exactly these members:
| member | form | meaning |
|---|---|---|
kind | string | "wai-data-export" |
version | integer | 1 |
account | token | the account whose records it holds |
part | token or null | the part of the account it is limited to; null for the whole account |
start, end | instants | the period whose records it holds: start before end |
made | instant | when the service made it: at or after end |
audit | object or null | the anchor of the audit chain it carries, of audit-chain §6’s form (seq, link, time); null for none |
3.2 A record line
A document of exactly kind, id and one of text and bytes:
| member | form | meaning |
|---|---|---|
kind | integer | the record’s kind, 1 to 127 (receipt-ledger §5.2) |
id | hash | the record’s identity under its kind |
text | string | a JSON record: its text, exactly as its issuer wrote it |
bytes | string | a binary record (WMR1, WLI1, …): its bytes in lowercase hexadecimal, at least one |
A JSON record is carried as its text, a string, as route-record §7.1’s verification document
carries the documents it is given, so that each is read as written: a record need not be in WAI’s
canonical subset, nor be canonical, to be carried. An OER/2 receipt of a failed hop states a work
status of −1, which no canonical form of this text admits; as a line’s text it is a string,
and the line is a document like any other. No WAI text extends the canonical form to carry it.
Records may come in any order the service chooses, but the audit entries in the order of their chain (§4); the order is signed (§3.4). A kind with no row in receipt-ledger §5.2 yet, from 1 to 127, is a record of a revision this verifier does not know: it is carried and counted, and read by whoever knows its kind.
3.3 The trailer
A document of exactly these members:
| member | form | meaning |
|---|---|---|
kind | string | "wai-data-export-end" |
version | integer | 1 |
count | integer | the number of record lines |
header | hash | the header’s digest |
records | hash | the records digest (§3.4) |
audit | object or null | the checkpoint over the audit chain it carries (audit-chain §5), a document as it stands; null exactly when the header’s audit is |
signer | key | the service’s key |
sig | signature | §3.4 |
3.4 Signature and digests
A record line’s digest is its digest (route-record §2): SHA-256 over the line without its newline, which is its canonical form. It names the record within the bundle: a deletion record names an exported record by it.
The records digest is SHA-256 over the 23 bytes wai:data-export-records (ASCII) and 0x01,
followed by the digest of each record line, 32 bytes each, in the order of the lines.
The trailer’s sig is Ed25519 (RFC 8032) by signer over the 15 bytes wai:data-export (ASCII)
and 0x01, followed by the canonical form of the trailer without sig. It is verified strictly, as
SPEC §7 Signatures requires: bad_key, bad_signature; a verifier given the keys
it holds for the service refuses any other signer (signer_unknown). The signature covers the
header through its digest and every record line through the records digest, so the service signs
the whole bundle, its order included.
A bundle’s digest, which names it (receipt-ledger §5.2, record kind 36), is its trailer’s
digest, sig included.
4. The audit chain it carries
A bundle whose header’s audit is an anchor carries the account’s audit chain from that anchor:
its entries are the record lines of kind 34 (audit-chain §3), each line’s text the entry’s
canonical form and its id the entry’s link (audit-chain §3.2); its checkpoint is the trailer’s
audit. A verifier holds the entries, in the order of their lines, to audit-chain §4 from the
anchor under the header’s account, and the checkpoint to audit-chain §5.2 and §5.4. So the chain
in a bundle is the chain of an audit export (audit-chain §6) in another frame: the anchor, the
entries and the checkpoint, the same documents and the same rules.
A bundle whose header’s audit is null carries no audit chain: its trailer’s audit is null
and it has no record line of kind 34.
5. Records WAI does not define
A bundle holds WAI’s records only: a record line of a private kind is refused (private_kind),
as wherever a WAI record states a kind (receipt-ledger §5.2). A service that hands an account’s
holder records of its own, which no WAI text defines (a provisional record carried beside WAI’s
until WAI defines one), hands them in a file of its own beside the bundle; that file MAY
name the bundle by its digest (§3.4), and is no part of it. A bundle’s signature, digests and
outcome say nothing about such a file.
6. Verification
6.1 Inputs
A verifier is given a bundle’s text and either the keys it holds for the service or none.
6.2 Steps
It runs these steps and stops at the first refusal, which it reports with its reason and where:
at, a list holding the index of the line, from 0, and for the checkpoint audit after it.
- The lines: a text that is not lines (§2), at the first line that is not one, or at the
line a missing final newline leaves unended (
export_malformed). - The header, at 0: a
kindother than"wai-data-export"isexport_malformed; aversionthat is an integer other than1isexport_unknown_version; a header not of §3.1’s members and forms, or whosestart,endandmadeare not in order, isexport_malformed. - The trailer, at the last line: a
kindother than"wai-data-export-end"isexport_malformed; aversionother than1,export_unknown_version; a trailer not of §3.3’s members and forms (itsauditan object ornull),export_malformed; then its signature,bad_key,bad_signature, and, when the verifier holds keys, its signer among them,signer_unknown. - The digests and the count, at the last line: the header’s digest is not the trailer’s
header(export_header_mismatch); the number of record lines is not itscount(export_count_mismatch); the records digest is not itsrecords(export_records_mismatch). - Each record line, in order, at its line: one not of §3.2’s members and forms, or whose
kindis not an integer from 1 to 255, isexport_malformed; a kind from 128 to 255 isprivate_kind; a kind and identity an earlier line states isexport_record_duplicate. - The audit chain (§4): a header’s
auditand a trailer’sauditof which one isnulland the other is not, at the last line, or a line of kind 34 in a bundle that carries no chain, at that line, isexport_audit_mismatch. Then, for a bundle that carries one, each line of kind 34 in order, at its line: one with notext, or whosetextis not a document in its canonical form, isaudit_malformed; then the entry’s form (audit-chain §3.1:audit_malformed,audit_unknown_version); anidthat is not the entry’s link,export_id_mismatch; then audit-chain §4’s rules. Then the checkpoint, at the last line andaudit: audit-chain §5.1 and §5.2 (audit_malformed,audit_unknown_version,bad_key,bad_signature,signer_unknown, under the keys the verifier holds), then §5.4.
6.3 Outcome
A bundle that passes is accepted, with its digest, its account, its part, its period, when it
was made, the number of its records and of each kind, the audit chain it carries (the anchor’s
sequence number, the number of entries, the checkpoint’s seq and head) or none, and its
signer. A verifier given no keys reports the signer; it has not checked whose it is.
What acceptance establishes: the service holding the signer’s key handed over exactly these
records, in this order, under this header, and the audit chain they carry holds from its anchor
to its checkpoint. What it does not: that any other record verifies (each is checked under its own
text, wai verify and the verifiers of route-record, receipt-ledger and the others), that a
record’s id is its identity under its kind (checked here for an audit entry alone, whose
identity this text computes as it holds the chain; a reader that verifies a record under its own
text holds its identity there), or that the bundle holds every record of the account (that is a
deletion record’s question, against a final bundle: data-deletion).
7. Reasons
| code | where |
|---|---|
export_malformed | §6.2 steps 1, 2, 3 and 5 |
export_unknown_version | §6.2 steps 2 and 3 |
bad_key | §3.4, §6.2 steps 3 and 6 |
bad_signature | §3.4, §6.2 steps 3 and 6 |
signer_unknown | §3.4, §6.2 steps 3 and 6 |
export_header_mismatch | §6.2 step 4 |
export_count_mismatch | §6.2 step 4 |
export_records_mismatch | §6.2 step 4 |
private_kind | §5, §6.2 step 5 |
export_record_duplicate | §6.2 step 5 |
export_audit_mismatch | §4, §6.2 step 6 |
export_id_mismatch | §6.2 step 6 |
The chain a bundle carries is refused with audit-chain §8’s codes, as §6.2 step 6 states.
8. Conformance
The reference implementation is the data_export module of wai-rs (feature data_export):
verify (§6) and the writers a service uses (header, record_line, entry_line, bundle).
wai verify checks every *.wai-export.jsonl under a path as §6 does, with no keys, naming the
signer. The corpus is live-conformance/data-export/,
written by wai_data_export_vectors gen from fixed seeds: bundles, each with its text, the keys a
verifier holds (or none) and its outcome; accepted ones among them (an account’s whole bundle with
its chain, after the chain’s retention, with no chain, of one part, of no record, with a chain and
no record, under keys not given, with a kind no row names yet, with one identity under two kinds),
and one refused case for each rule of §3, §4 and §6. Its records other than audit entries are
synthetic stand-ins, opaque test texts and bytes no case reads, since no rule of this text reads
them: an OER/2 receipt’s text with a work status of −1 among them. tests/data_export.rs requires
the file to regenerate byte for byte, every case to get its stated outcome, and §7 to list exactly
the codes the reference reports. live-conformance/data_export_verify.py is the second
implementation, written from this text and sharing no code with wai-rs: it reads documents with
route_record_verify.py, the chain with audit_chain_verify.py and signatures with the shared
_ed25519.py, requires every stated outcome, mutates copies of the corpus and requires each to
fail, and drops each of its own checks in turn and requires the corpus to catch it.
9. Security and privacy considerations
- What a bundle proves. Given the service’s key: what it handed over, whole and in order. Not that the records are true or complete; a service that leaves a record out of a bundle signs a bundle without it. An account’s holder that holds an earlier bundle holds the audit chain’s earlier checkpoint, and audit-chain §7.2 holds the later chain to it.
- What a bundle carries. Everything the account’s records carry, whatever names and values the service and the records’ issuers put in them. A bundle is the account holder’s copy of its own records; WAI sets no rule on to whom a service hands one or how it learns who is asking, and none on what the holder does with it.
- A bundle of a part. A bundle whose
partis notnullholds the records of that part, as the service divides its account; nothing in it shows what other parts hold.