Skip to main content

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 the data_export module of wai-rs (feature data_export), and wai verify reads a bundle by its name; the corpus is live-conformance/data-export/, and its second implementation is live-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

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:

memberformmeaning
kindstring"wai-data-export"
versioninteger1
accounttokenthe account whose records it holds
parttoken or nullthe part of the account it is limited to; null for the whole account
start, endinstantsthe period whose records it holds: start before end
madeinstantwhen the service made it: at or after end
auditobject or nullthe 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:

memberformmeaning
kindintegerthe record’s kind, 1 to 127 (receipt-ledger §5.2)
idhashthe record’s identity under its kind
textstringa JSON record: its text, exactly as its issuer wrote it
bytesstringa 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:

memberformmeaning
kindstring"wai-data-export-end"
versioninteger1
countintegerthe number of record lines
headerhashthe header’s digest
recordshashthe records digest (§3.4)
auditobject or nullthe checkpoint over the audit chain it carries (audit-chain §5), a document as it stands; null exactly when the header’s audit is
signerkeythe service’s key
sigsignature§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.

  1. 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).
  2. The header, at 0: a kind other than "wai-data-export" is export_malformed; a version that is an integer other than 1 is export_unknown_version; a header not of §3.1’s members and forms, or whose start, end and made are not in order, is export_malformed.
  3. The trailer, at the last line: a kind other than "wai-data-export-end" is export_malformed; a version other than 1, export_unknown_version; a trailer not of §3.3’s members and forms (its audit an object or null), export_malformed; then its signature, bad_key, bad_signature, and, when the verifier holds keys, its signer among them, signer_unknown.
  4. 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 its count (export_count_mismatch); the records digest is not its records (export_records_mismatch).
  5. Each record line, in order, at its line: one not of §3.2’s members and forms, or whose kind is not an integer from 1 to 255, is export_malformed; a kind from 128 to 255 is private_kind; a kind and identity an earlier line states is export_record_duplicate.
  6. The audit chain (§4): a header’s audit and a trailer’s audit of which one is null and the other is not, at the last line, or a line of kind 34 in a bundle that carries no chain, at that line, is export_audit_mismatch. Then, for a bundle that carries one, each line of kind 34 in order, at its line: one with no text, or whose text is not a document in its canonical form, is audit_malformed; then the entry’s form (audit-chain §3.1: audit_malformed, audit_unknown_version); an id that is not the entry’s link, export_id_mismatch; then audit-chain §4’s rules. Then the checkpoint, at the last line and audit: 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

codewhere
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