taler-docs

Documentation for GNU Taler components, APIs and protocols
Log | Files | Refs | README | LICENSE

commit d6ad9388d1e90f35b1355377630919c153a6c9e9
parent 1d09311a4cbcd24f5734cb875b26e3d1adab17d3
Author: Iván Ávalos <avalos@disroot.org>
Date:   Fri,  7 Aug 2026 14:00:53 +0200

dd92: document stored block signatures, relink signatures and block serial

Diffstat:
Mdesign-documents/092-incremental-backup-sync.rst | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
1 file changed, 70 insertions(+), 20 deletions(-)

diff --git a/design-documents/092-incremental-backup-sync.rst b/design-documents/092-incremental-backup-sync.rst @@ -138,13 +138,21 @@ Block format ++++++++++++ Each block will consist of a 2-byte version number, a random 24-byte nonce, -and a gzip-compressed JSON object with its length. The block will be padded up -to the next whole kilobyte for privacy reasons. A block whose length is -already a multiple of a kilobyte is not padded further. +an 8-byte serial, and a gzip-compressed JSON object with its length. The +block will be padded up to the next whole kilobyte for privacy reasons. A +block whose length is already a multiple of a kilobyte is not padded further. The nonce is 24 bytes because that is exactly what `secretbox`_ takes, which lets a block be encrypted under its own nonce. +The serial is only ever seen by the wallets: it sits inside the encrypted +payload, so the sync server knows nothing about it. Wallets assign it on +every content write (append or in-place update) as the account's maximum +known serial plus one; relinking a block never changes its data and therefore +never its serial. A wallet checks the serial when it decrypts a block and +refuses to apply a block whose serial is lower than the last one it saw for +that block, which makes a rolled-back (replayed) block detectable. + Encryption will be performed on the block using symmetric authenticated encryption via libsodium's `secretbox`_ function, with a 32-byte key derived from the wallet's backup encryption key and the nonce of the block, which in @@ -165,6 +173,8 @@ user wishes to add to the synchronization group. +----------------------------+ | nonce (24 byte) | +----------------------------+ + | serial (8 byte) | + +----------------------------+ | JSON length n (4 byte) | +----------------------------+ | gzipped JSON (n byte) | @@ -264,6 +274,23 @@ references hashes the empty byte string. A UID may appear at most once, since the wire format keys the references by UID and could not otherwise transmit them faithfully. +The server stores the ``upload_sig`` with the block, together with the +rest of the signed context (``old_hash`` and ``refs_hash``), and returns +them in the block list. A wallet therefore verifies every block's +stored signature against the account key before applying it; a block +whose signature does not verify must not be applied. + +Operations that rewrite the links of an existing block (an append +relinks the previous tail, a delete relinks both of its neighbours) +require that block's *new* signature to be uploaded along with the +operation. This is an ordinary ``TALER_SIGNATURE_SYNC_BLOCK_UPLOAD`` +signature over the relinked block's new nonces, carried in the +``relink_prev`` / ``relink_next`` fields of the request. The server +verifies it against the current state of the relinked block and stores +it in the block's row; relinking never changes the block's data, so the +signature's ``old_hash`` and ``new_hash`` are both the block's stored +hash. + .. http:get:: /config Return the server's protocol version and terms. Requires no account @@ -321,20 +348,26 @@ by UID and could not otherwise transmit them faithfully. .. ts:def:: BlockEntry interface BlockEntry { - nonce: string; - block_hash: string; - prev_nonce?: string; - next_nonce?: string; + nonce: BlockUuid; + block_hash: HashCodeString; + prev_nonce?: BlockUuid; + next_nonce?: BlockUuid; data: string; + upload_sig: EddsaSignatureString; + old_hash: HashCodeString; + refs_hash: HashCodeString; } ``data`` is the encrypted block payload as it was uploaded, and hashes to ``block_hash``. ``prev_nonce`` and ``next_nonce`` are absent for the first and last block of the linked list respectively. + ``upload_sig`` is the signature stored with the block, and + ``old_hash`` / ``refs_hash`` the remainder of the signed context; the + wallet verifies the signature before applying the block. .. http:post:: /backups/${ACCOUNT_KEY}/blocks/${NONCE} - Upload a new block and append (or insert) it into the account's linked + Upload a new block and append it at the end of the account's linked list. If a block with the same nonce already exists, the content hash is compared: if it matches, a ``304 Not modified`` is returned; if it differs, the client should use ``PUT`` instead. @@ -365,11 +398,12 @@ by UID and could not otherwise transmit them faithfully. .. code-block:: typescript interface UploadBlockRequest { - upload_sig: string; - prev_nonce?: string; - next_nonce?: string; + upload_sig: EddsaSignatureString; + prev_nonce?: BlockUuid; + next_nonce?: BlockUuid; data: string; - object_refs?: { [uid: string]: number }; + object_refs?: { [uid: BlobUid]: number }; + relink_prev?: { upload_sig: EddsaSignatureString }; } ``upload_sig`` @@ -384,8 +418,8 @@ by UID and could not otherwise transmit them faithfully. Must be omitted for the first block. ``next_nonce`` - Nonce of the succeeding block in the DLL. - Must be omitted for the last block. + Must be omitted; inserts into the middle of the linked list are not + supported, so an append never has a succeeding block. ``data`` The encrypted block contents (binary, base32-encoded). @@ -400,6 +434,12 @@ by UID and could not otherwise transmit them faithfully. the account does not have, or would take a reference count below zero, the entire request is rejected and nothing is modified. + ``relink_prev`` + Required when ``prev_nonce`` is present. The new signature of the + block at ``prev_nonce`` (the previous tail), covering its new + ``next`` link after this append. The server verifies it against + the tail's current state and stores it with the block. + **Response** :http:statuscode:`204 No content`: @@ -464,10 +504,12 @@ by UID and could not otherwise transmit them faithfully. .. code-block:: typescript interface DeleteBlockRequest { - delete_sig: string; - prev_nonce?: string; - next_nonce?: string; - object_refs?: { [uid: string]: number }; + delete_sig: EddsaSignatureString; + prev_nonce?: BlockUuid; + next_nonce?: BlockUuid; + object_refs?: { [uid: BlobUid]: number }; + relink_prev?: { upload_sig: EddsaSignatureString }; + relink_next?: { upload_sig: EddsaSignatureString }; } ``delete_sig`` @@ -493,6 +535,14 @@ by UID and could not otherwise transmit them faithfully. an adjustment names an unknown object or would take a reference count below zero. + ``relink_prev`` + Required when ``prev_nonce`` is present. The new signature of the + block at ``prev_nonce``, covering its new ``next`` link. + + ``relink_next`` + Required when ``next_nonce`` is present. The new signature of the + block at ``next_nonce``, covering its new ``prev`` link. + **Response** :http:statuscode:`204 No content`: @@ -595,7 +645,7 @@ account that uploaded it. .. ts:def:: ObjectEntry interface ObjectEntry { - uid: string; + uid: BlobUid; data: string; } @@ -616,7 +666,7 @@ account that uploaded it. .. code-block:: typescript interface UploadObjectRequest { - object_sig: string; + object_sig: EddsaSignatureString; data: string; }