xftp server: support storage time and BBS proofs of badge credential to extend it

This commit is contained in:
Evgeny @ SimpleX Chat
2026-08-22 21:47:38 +00:00
parent ac4bf690b6
commit 4989bd8df6
2 changed files with 169 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
# Implementation plan: XFTP variable file storage time
Proposal: `../rfcs/2026-08-22-xftp-file-storage-time.md`.
## simplexmq: entitlement crypto
New module `Simplex.Messaging.Crypto.Entitlement`, over `Simplex.Messaging.Crypto.BBS`:
- `Entitlement {level :: Text, expiresAt :: UTCTime, extraInfo :: Text}`
- `EntitlementCredential` (issuer key index, holder secret, BBS signature, entitlement)
- `EntitlementProof` (issuer key index, presentation header, BBS proof, entitlement)
- the disclosed-message encoding: the holder secret is message 0 and stays undisclosed; `expiresAt`, `level`, and `extraInfo` are messages 1 to 3 and are disclosed
- the BBS header string `"SimpleX entitlement v1"`, the message count, and the disclosed indexes
- `generateEntitlementProof :: BBSPublicKey -> EntitlementCredential -> BBSPresHeader -> IO (Either String EntitlementProof)`
- `verifyEntitlement :: Map Int BBSPublicKey -> EntitlementProof -> IO (Maybe Bool)`
- the issuer public keys constant `Map Int BBSPublicKey`
## simplexmq: protocol, new XFTP version
In `Simplex.FileTransfer.Transport`:
- add the next `VersionXFTP` and set `currentXFTPVersion` to 4
In `Simplex.FileTransfer.Protocol`:
- add `FileStorageTime` and its encoding
- add the `FileStorageTime` and `Maybe EntitlementProof` fields to `FNEW`, and add `FTTL`
- add the expiration to `FRSndIds`, and add a new response for `FTTL`
- build the presentation header for FNEW and for FTTL
In `Simplex.FileTransfer.Server`:
- pass `sessionId` from `thParams` into `processXFTPRequest`
## simplexmq: server configuration
In `Simplex.FileTransfer.Server.Env` and `Simplex.FileTransfer.Server.Main`:
- read a maximum storage time for each entitlement level, and a default maximum, from the INI file
- exit at startup if any level maximum is below the default
- read the issuer public keys from the shared constant
## simplexmq: server store and expiration
Common to both stores, in `Simplex.FileTransfer.Server.Store`:
- add `expiresAt` to `FileRec`
- in `createFile`, verify the proof against `sessionId <> sndKey <> digest`, resolve the maximum from the level, set `expiresAt = now + min(requested, maximum)`, and return the expiration
- add the FTTL handler, which verifies the proof against `sessionId <> senderId`, sets `expiresAt = now + min(requested, maximum)`, and returns the expiration
- retain `created_at` for statistics and export
STM store:
- in `expiredFiles`, select files where `expiresAt < now`
PostgreSQL store, in `Simplex.FileTransfer.Server.Store.Postgres` and its migrations:
- add the column `expires_at BIGINT NOT NULL`
- add a migration for the column and the index `idx_files_expires_at`
- change the `expiredFiles` query to `WHERE expires_at < ? ORDER BY expires_at LIMIT ?`
Store log, in `Simplex.FileTransfer.Server.StoreLog`:
- add `expiresAt` to the `AddFile` record
- for older records without an expiration, default `expiresAt` to `createdAt + default storage time`
## simplexmq: agent
Public API in `Simplex.Messaging.Agent`:
- add `Maybe EntitlementCredential` and `FileStorageTime` parameters to `xftpSendFile` and `xftpSendDescription`
- add a set-time API for FTTL that operates per chunk, using the sender description
Store, in both the SQLite and PostgreSQL agent stores:
- add a nullable entitlement credential column and a storage time column to `snd_files`
- add the migration to both stores
- in `createSndFile`, store the credential and the storage time
Upload, in `Simplex.Messaging.Agent.Client` and `Simplex.FileTransfer.Client`:
- in `agentXFTPNewChunk`, read the credential, the storage time, and the digest from the send record
- inside `withClient`, where `sessionId` is available, build the presentation header `sessionId <> sndKey <> digest`, generate the proof, and send FNEW with the storage time and the proof
- discard the returned expiration for now
Set-time:
- for a completed file, generate a per-chunk proof bound to `sessionId <> senderId` and send FTTL, authorized with the sender key
## simplex-chat
- remove lifetime badges: make `badgeExpiry` a `UTCTime`, drop the `"lifetime"` encoding, and remove the lifetime option from the UI and the CLI
- map `BadgeInfo` to `Entitlement` (`level = textEncode badgeType`, `expiresAt = badgeExpiry`, `extraInfo = badgeExtra`) when calling the agent
- pass the user's credential and `FSTMax` to `xftpSendFile`
- retain the `maxXFTPFileSize` size limit
- reuse `verifyEntitlement` for peer-badge verification
- import the issuer public keys from the shared simplexmq constant
## Order
1. Add the entitlement crypto module; move chat's badge verification onto it and remove lifetime badges.
2. Add `FileStorageTime`, the new XFTP version, the FNEW and FTTL protocol changes, and the responses.
3. Change the server configuration, store, expiration, and store log.
4. Change the agent store, add proof generation on upload, and add the set-time API.
5. Wire chat to pass the credential and the storage time.
+64
View File
@@ -0,0 +1,64 @@
# XFTP variable file storage time
## Summary
The server stores a storage time for each file. The sender sets it in the FNEW command and resets it with a new FTTL command. The sender may present a proof of an entitlement to raise the maximum storage time the server allows. Each proof is bound to the uploaded chunk and to the TLS session, so it cannot be reused for another chunk or another session.
## Entitlement
An entitlement is a level, an expiration, and an extra string:
```
data Entitlement = Entitlement
{ level :: Text,
expiresAt :: UTCTime,
extraInfo :: Text
}
```
The entitlement is the disclosed content of a BBS proof: the holder's secret remains undisclosed, and the three fields are revealed. The server interprets `level` to select a maximum storage time and ignores `extraInfo`; interpretation of `extraInfo` is out of scope here.
The protocol layers know only the entitlement, never a badge. Chat maps its own badge to an entitlement before it asks the agent to send.
simplexmq defines the entitlement, its BBS proof generation and verification, the disclosed-message encoding, and the issuer public keys, in `Simplex.Messaging.Crypto.Entitlement`. The protocol form is `EntitlementProof` (the issuer key index, the presentation header, the BBS proof, and the entitlement). The signing form is `EntitlementCredential` (the issuer key index, the holder secret, the BBS signature, and the entitlement); the server never receives it.
## Storage time
```
data FileStorageTime = FSTMax | FSTFor Word32 -- hours
```
`FSTFor` requests a specific number of hours. `FSTMax` requests the maximum the server allows for the presented entitlement, or the default maximum when no proof is present.
## Commands, new XFTP version
FNEW gains a storage time and an optional entitlement proof:
```
FNEW FileInfo (NonEmpty RcvPublicAuthKey) (Maybe BasicAuth) FileStorageTime (Maybe EntitlementProof)
```
FTTL is a new `FileCommand FSender`, authorized with the sender key of `senderId`:
```
FTTL FileStorageTime (Maybe EntitlementProof)
```
FTTL sets the expiration to `now + min(requested, maximum)`. It may reduce the current expiration, since the sender can also delete the file.
Each command binds its proof to a presentation header:
- FNEW: `sessionId <> sndKey <> digest`
- FTTL: `sessionId <> senderId`
Both commands return the granted expiration: FNEW extends the `FRSndIds` response with it, and FTTL uses a new response that returns it.
Version 3 and earlier encode neither the storage time nor the proof, and the server applies the default storage time. `currentXFTPVersion` becomes 4.
## Maximum storage time
The server configures a maximum storage time for each entitlement level, and a default maximum for requests with no proof. The server exits at startup if any level maximum is below the default, so a proof never reduces the allowed time. The server treats an entitlement whose `expiresAt` has passed as no proof.
## Binding
The presentation header binds each proof to the TLS session and to the specific chunk. On FNEW the chunk is identified by the sender key and the digest, which the server already verifies for every later command on the file. On FTTL the chunk is identified by `senderId`, which the server has assigned by then. A proof generated for one session and chunk verifies for no other, which prevents reuse.