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:
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;
}