WAI Extension: Data Deletion
Mirrored from the canonical text at commit 0d3f0d7c ().
Status: Draft. Registers the record
wai.data.deletion: what a service deleted of a subject’s records, and what it kept and why, so that the subject can check a deletion against the final export it holds rather than take it on the service’s word. A deletion record (§3) names the subject, the basis of the deletion and when it arose, when it was done, the subject’s final data export by its digest and the head of the subject’s audit chain. It states the subject’s records in classes the service names, each deleted or kept, each by its count and the RFC 6962 tree hash over its leaves, the sorted digests of its records (§4); a kept class states its reason, in words, and when it ends. It names the keys whose private halves were destroyed, and each other party that held any of it, asked to delete or under its own stated retention, citing by link hash the purges of a takedown where that party signed one, and when the last backup that may hold any of it expires. The leaves are served beside the record (§5). A verifier given the record, its leaves and the final export checks the signature, every class’s count and tree hash, no leaf in two classes, the times in order, that the export is the subject’s whole and made after the basis arose, that every record of it lies in exactly one class, that every leaf of a class the export holds is in it, and that the audit head is at or after the export’s checkpoint (§6). WAI decides no legal question and sets no rule on what a service must delete or may keep: it states what the service says it did, checkably. The reference implementation is thedata_deletionmodule ofwai-rs(featuredata_deletion); the corpus islive-conformance/data-deletion/, and its second implementation islive-conformance/data_deletion_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.Texts read for this revision: the WAI texts this one cites; RFC 6962 §2.1 (the Merkle tree hash); RFC 8259; RFC 8785; RFC 8032; FIPS 180-4.
1. Scope
In scope: the deletion record, its signature and its digest (§3); its classes and their tree hashes (§4); its leaves (§5); and what a verifier checks of it against the subject’s final export (§6).
Out of scope: when a service must delete and what it may keep, which law, contract or policy it reads, and what words it gives; how the subject asks and how the service learns who asks; how a deletion is done; and what a record of another text means. A class’s name, a kept class’s words and a holder’s identifier are the service’s, opaque to every verifier.
The deletion of a media object a party still holds, in answer to a notice or of the service’s
own initiative, is takedown’s: each holder signs a purge of what it no longer holds.
A deletion record does not replace one: it names a subject’s records (receipts, invoices, its
audit chain, the rows a service keeps of it, blobs), states what was kept beside what was deleted,
and cites a takedown’s purges where a holder signed one (§3.1, holders).
Relationship to the core spec: wai.data.deletion 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, instant, word: as audit-chain §2.
- Subject: a token naming whose records are deleted: an account of data-export §2. WAI says nothing about who or what a subject is.
- Final export: the subject’s data export (data-export §3) made for the deletion: of the whole
subject (its
partnull) and made at or after the basis arose. It is the export the subject holds and checks the deletion against. - Leaf: the 32-byte digest that names one record in a class (§4).
- Service: the party that held the records, deleted them and signs the record.
3. The deletion record
3.1 Members
A deletion record is a document of exactly these members:
| member | form | meaning |
|---|---|---|
kind | string | "wai-data-deletion" |
version | integer | 1 |
subject | token | whose records |
basis | string | termination (the subject’s account ended), request (a deletion was asked for) or retention (a retention period ended) |
arose | instant | when the basis arose: the closing, the request, the period’s end |
deleted_at | instant | when the deletion was done |
export | hash | the final export’s digest (data-export §3.4) |
audit | object or null | exactly seq, at least 1, and head, a hash: the last entry of the subject’s audit chain when the deletion was done (audit-chain §3); null for a subject with none |
deleted | array | 0 to 256 classes, in ascending order of class, each exactly class, a word, count, an integer of at least 1, and root, a hash (§4) |
kept | array | 0 to 256 classes as deleted’s, each with reason, statute, published or record (§3.3), why, a string of 1 to 1024 bytes, and until, an instant, when keeping it ends |
keys | array | 0 to 1024 objects of exactly use, a word, and key, a key, in ascending order of key: public keys whose private halves were destroyed (a buyer’s derived key, a room’s, a hosted channel’s), each with what it was for |
holders | array | 0 to 1024 objects of exactly holder, a token, action, until and purges, in ascending order of holder: each other party that held any of the subject’s records or objects; action asked (it was asked to delete them; until null) or retention (its stated retention applies; until the instant it ends); purges, 0 to 4096 hashes in ascending order, the link hashes of the takedown purges it signed for them (takedown §2, §5.5) |
backups_clear | instant | when the last backup that may hold any of the records expires |
signer | key | the service’s key |
sig | signature | §3.2 |
Every class is named once, in deleted or in kept. A class holds at least one record; a
service that deleted nothing of a kind states no class for it.
3.2 Signature and digest
A record’s sig is Ed25519 (RFC 8032) by signer over the 17 bytes wai:data-deletion (ASCII)
and 0x01, followed by the canonical form of the record 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). A record’s digest, which
names it (receipt-ledger §5.2, record kind 37, and §5), is the digest of the whole record, sig
included.
3.3 What is kept, and why
A kept class’s reason is one of three, and its why says it in the service’s words:
statute: a law, a regulation or a contract the service names inwhyrequires it kept (a ledger kept for a statutory period). WAI decides no such question; the service states it.published: it was published, and is kept as published (aggregates in a transparency report).record: the same bytes are named by another subject’s records, or a takedown keeps them; they are kept for that, and are no deletion that did not happen.
Every kept class ends: until is after deleted_at. A class kept with no end is not a deletion,
and is not stated as one.
4. Classes and their tree hashes
A class’s records are named by their leaves: for a record the final export held, its record line’s digest (data-export §3.4), so that the subject checks the class against the export it holds; for anything else (a blob, a row no export carries, a record made after the final export, such as the audit entries of the deletion itself), SHA-256 over its bytes, a document’s canonical form. A class’s leaves are its records’ leaves in ascending order, none twice.
Its count is the number of its leaves, and its root is the Merkle tree hash of RFC 6962 §2.1
over them, each leaf’s 32 bytes the input of one leaf: the hash of one leaf is SHA-256 over 0x00
and the leaf, and the hash of n > 1 leaves is SHA-256 over 0x01, the hash of the first k leaves and
the hash of the rest, k the largest power of two less than n. A node is never paired with itself.
A record made after the final export is a class of its own (§6.2 step 5 reads why): a class of records the export holds holds them all.
5. The leaves
The service serves a record’s leaves beside it: a document of exactly these members:
| member | form | meaning |
|---|---|---|
kind | string | "wai-data-deletion-leaves" |
version | integer | 1 |
deletion | hash | the record’s digest (§3.2) |
classes | array | one object for each class of the record, deleted’s then kept’s, each in its list’s order, of exactly class, its name, and leaves, its leaves as hashes, in ascending order, none twice |
The leaves are digests only: they name nothing a reader does not already hold. They are also the list of what a restored backup is held to: a record restored whose digest is a leaf of a deleted class is deleted again.
6. Verification
6.1 Inputs
A verifier is given a record’s text, its leaves’ text, the final export’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: record, leaves and the index of the class (and of a leaf in it), or export and,
for the export’s own refusals, data-export §6.2’s place within it.
- The record, at
record: a text that is not a document, akindother than"wai-data-deletion", or a record not of §3.1’s members and forms, isdeletion_malformed; aversionthat is an integer other than1isdeletion_unknown_version(before the members are read); then its signature and signer (§3.2). Then, in order:arose,deleted_atandbackups_clearnot in that order, each at or after the one before (deletion_times_invalid); a class named twice indeletedandkepttogether (deletion_class_repeated); a kept class whoseuntilis not afterdeleted_at(deletion_kept_ended). - The leaves, at
leaves: a text that is not a document, akindother than"wai-data-deletion-leaves", or leaves not of §5’s members and forms, isdeletion_malformed; aversionother than1,deletion_unknown_version; adeletionthat is not the record’s digest, or classes that are not the record’s, by name and in its order (deletion_leaves_mismatch). Then each class in turn, atleavesand its index: its number of leaves not itscount(deletion_count_mismatch); their tree hash (§4) not itsroot(deletion_root_mismatch); a leaf an earlier class holds (deletion_leaf_repeated). - The final export, at
export: it verifies under data-export §6 with the keys the verifier holds, its reason atexportand the place within it; its digest is not the record’sexport, or its account not the record’ssubject(deletion_export_mismatch); it is of a part, not the whole (deletion_export_partial); it was made before the basis arose (deletion_export_early). - Every record of the export, in order, at
exportand its line: its record line’s digest is a leaf of no class (deletion_record_unaccounted). The export’s header and trailer, and the audit checkpoint the trailer carries, are its frame, not its records: they are in no class. - Every leaf of a class the export holds: a class with a leaf the export holds, that has a
leaf the export does not hold, at
leaves, the class’s index and the leaf’s (deletion_leaf_not_exported). So an export made early, or of part of the subject’s records, does not pass as final, and a record made after the final export is in a class of its own (§4). - The audit head, at
recordandaudit, when the export carries an audit chain: the record’sauditisnull, or itsseqbelow the export’s checkpoint’s (deletion_audit_behind); itsseqthe checkpoint’s and itsheadnot the checkpoint’s (deletion_audit_mismatch). A head after the checkpoint is the chain grown after the export, the entries of the deletion among them, which the record’s classes name by their bytes (§4).
6.3 Outcome
A record that passes is accepted, with its digest, its subject, the number of records it deleted and of those it kept (the sums of its classes’ counts), the number of the export’s records it accounts for, the export’s digest and its signer.
What acceptance establishes: given the service’s key, the service states that it deleted every record of the deleted classes and kept those of the kept classes until their ends, each named by a leaf the subject can hold against what it holds; and every record the subject was handed in its final export is in that statement, once. What it does not: that the bytes are gone. No verifier can see a deletion; the record turns “we deleted it” into a statement that is signed, complete against the export, and contradicted by any copy that turns up later.
7. Reasons
| code | where |
|---|---|
deletion_malformed | §6.2 steps 1 and 2 |
deletion_unknown_version | §6.2 steps 1 and 2 |
bad_key | §3.2, §6.2 step 1 |
bad_signature | §3.2, §6.2 step 1 |
signer_unknown | §3.2, §6.2 step 1 |
deletion_times_invalid | §6.2 step 1 |
deletion_class_repeated | §3.1, §6.2 step 1 |
deletion_kept_ended | §3.3, §6.2 step 1 |
deletion_leaves_mismatch | §5, §6.2 step 2 |
deletion_count_mismatch | §4, §6.2 step 2 |
deletion_root_mismatch | §4, §6.2 step 2 |
deletion_leaf_repeated | §6.2 step 2 |
deletion_export_mismatch | §6.2 step 3 |
deletion_export_partial | §6.2 step 3 |
deletion_export_early | §6.2 step 3 |
deletion_record_unaccounted | §6.2 step 4 |
deletion_leaf_not_exported | §6.2 step 5 |
deletion_audit_behind | §6.2 step 6 |
deletion_audit_mismatch | §6.2 step 6 |
A final export that does not verify is refused with data-export §7’s codes, and its chain with audit-chain §8’s, as §6.2 step 3 states.
8. Conformance
The reference implementation is the data_deletion module of wai-rs (feature data_deletion):
verify (§6), tree_hash (§4), and the writer a service uses (Deletion, its record and
leaves). The corpus is live-conformance/data-deletion/,
written by wai_data_deletion_vectors gen from fixed seeds: deletions, each with its record, its
leaves, the final export, the keys a verifier holds (or none) and its outcome; accepted ones among
them (an account closed and deleted with classes kept under each reason, an audit head at the
export’s checkpoint and one after it, a subject with no audit chain, nothing kept, no key and no
holder, keys not given), and one refused case for each rule of §3 to §6. Its exported records other
than audit entries, and its blobs, are synthetic stand-ins, as data-export’s corpus has them.
tests/data_deletion.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_deletion_verify.py is the second implementation, written from this text and
sharing no code with wai-rs: it reads the export with data_export_verify.py, documents with
route_record_verify.py and signatures with the shared _ed25519.py, computes RFC 6962’s tree hash
itself, 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 deletion record proves. That the service states, under its key, what it deleted and kept, completely against the export the subject holds. Not that the bytes are gone from every disk, and not that a holder honoured the request it was sent: a holder’s own purge, where one exists, is the evidence of that (takedown §5.5), and the record cites it.
- Leaves, not records. A deletion record and its leaves carry digests and the service’s words, never a record’s contents: publishing them reveals that records existed and how many, not what they held. A class’s name and a kept class’s words are the service’s; a service that names a person in them names them in a record it hands on.
- A short export. An export that leaves a record out of a class the export otherwise holds fails §6.2 step 5. A service that leaves out a whole class of records, and names it in no class, is caught only by a subject that holds an earlier export with such records, which it checks against the record’s classes itself; and the audit chain the export carries holds the record’s head to the export’s checkpoint (§6.2 step 6).
- Restored backups. The leaves of the deleted classes are what a restore is held to (§5); a
service that restores a backup before
backups_cleardeletes again every record whose digest is a leaf of a deleted class.