This document is the authoritative technical specification for the DistFS protocol. It provides every detail necessary for a clean-room implementation of a Server (Metadata/Data) or Client.
- Identity/Signing: ML-DSA-65 (FIPS 204). Signature size: 2420 bytes.
- Asymmetric Encryption: ML-KEM-768 (FIPS 203). Ciphertext size: 1088 bytes.
- Symmetric Encryption: AES-256-GCM. 12-byte random nonces. 16-byte tags.
- Hashing: SHA-256.
- JSON: All JSON payloads used in signatures or encryption MUST be minified (no whitespace) and use lexicographically sorted keys (JCS).
- Binary: Multi-byte integers are Big-Endian by default unless specified otherwise.
Provides end-to-end encryption for sensitive Metadata API requests.
Used for initial login and requests where no session is established.
Inner Payload Format (Binary):
[Timestamp (8b BigEndian uint64)][Signature (2420b ML-DSA-65)][Plaintext JSON]
- Signature: Calculated over
[Timestamp][Plaintext JSON].
Encrypted Wrapper (Binary):
[KEM_CT (1088b)][NONCE (12b)][AES_GCM_CT + TAG]
- KEM_CT: Encapsulates a 32-byte Shared Secret for the recipient's ML-KEM-768 public key.
- AES_GCM_CT: The Inner Payload encrypted using the Shared Secret.
Used for requests within an established session. Provides improved performance and forward secrecy.
Encrypted Wrapper (Binary):
[KEM_CT (1088b zeroed)][NONCE (12b)][AES_GCM_CT + TAG]
- KEM_CT: MUST be zero-filled to signal symmetric mode.
- AES_GCM_CT: The Inner Payload encrypted using the ephemeral Session Key derived during
Login.
HTTP Request/Response Wrappers:
- Request:
{"uid": "user_id", "sealed": "base64_enc_wrapper"} - Response:
{"sealed": "base64_enc_wrapper"} - Header:
X-DistFS-Sealed: trueMUST be set for all sealed requests. - Header:
Session-TokenMUST be provided for symmetric mode.
These endpoints remain accessible on their original paths to support bootstrapping and cluster coordination.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/v1/health |
None | Node health status. |
GET |
/v1/meta/key |
None | Cluster ML-KEM Public Key. |
GET |
/v1/meta/key/sign |
None | Cluster ML-DSA Public Key. |
GET |
/v1/meta/key/world |
None | Public Key for the 'world' identity. |
GET |
/v1/node/info |
None | Node ID and protocol version. |
GET |
/v1/auth/config |
None | OIDC Issuer and endpoint configuration. |
GET |
/v1/cluster/stats |
None | Cluster-wide storage and node metrics. |
POST |
/v1/user/register |
OIDC JWT | Register a new user and public keys. |
POST |
/v1/auth/challenge |
None | Request a login challenge for a User ID. |
POST |
/v1/login |
Challenge | Establish a session and establishment a shared secret. |
POST |
/v1/user/keysync |
Session + E2EE | Store encrypted configuration backup. |
GET |
/v1/user/keysync |
Bearer JWT | Retrieve encrypted configuration backup. |
POST |
/v1/node |
Raft Secret | Internal cluster management. |
To prevent network analysis and preserve user privacy, all authenticated metadata and identity operations are routed through a single, non-descript endpoint.
- Endpoint:
POST /v1/invoke - Authentication: Required (Layer 7 E2EE + Session Token)
- Request Format:
SealedEnvelope(Decrypted inner payload) - Response Format:
SealedResponse(Encrypted)
{
"action": "ActionName",
"payload": { ... }
}| Action | Description |
|---|---|
GetInode |
Fetch metadata for a specific inode. Payload: {"id": "hex"}. |
GetInodes |
Fetch metadata for multiple inodes. Payload: InodeIDBatchRequest. |
IssueToken |
Request a storage access token. Payload: TokenRequest. |
Batch |
Apply a sequence of Raft commands atomically. Payload: []LogCommand. |
AllocateChunk |
Request storage nodes for a new chunk. Payload: {}. |
AcquireLeases |
Acquire distributed leases. Payload: AcquireLeaseRequest. |
ReleaseLeases |
Release distributed leases. Payload: ReleaseLeaseRequest. |
GetUser |
Fetch user profile and usage. Payload: {"id": "hex"}. |
ListGroups |
List groups the user belongs to. Payload: {}. |
GetGroup |
Fetch group metadata and registry. Payload: {"id": "hex"}. |
AllocateGID |
Allocate a new Global Group ID. Payload: {}. |
GetWorldPrivate |
Fetch the private key for the "world" group. Payload: {}. |
AdminUsers |
List all users (Admin only). Payload: {}. |
AdminGroups |
List all groups (Admin only). Payload: {"cursor": "...", "limit": int}. |
AdminLeases |
List all active leases (Admin only). Payload: {}. |
AdminNodes |
List all storage nodes (Admin only). Payload: {}. |
AdminStatus |
Get cluster health and stats (Admin only). Payload: {}. |
AdminPromote |
Promote a user to Admin (Admin only). Payload: {"user_id": "hex"}. |
AdminUserLock |
Lock/Unlock a user account (Admin only). Payload: AdminSetUserLockRequest. |
AdminUserQuota |
Set user storage quota (Admin only). Payload: SetUserQuotaRequest. |
AdminGroupQuota |
Set group storage quota (Admin only). Payload: SetGroupQuotaRequest. |
AdminClusterJoin |
Join a new node to the cluster (Admin only). Payload: {"address": "url"}. |
AdminClusterRemove |
Remove a node from the cluster (Admin only). Payload: {"id": "hex"}. |
AdminAudit |
Stream cluster audit logs (Admin only). Payload: {}. |
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/v1/data/{id} |
Signed Token | Download encrypted chunk. |
PUT |
/v1/data/{id} |
Signed Token | Upload encrypted chunk. Optional Query: replicas=csv_node_addresses. |
DELETE |
/v1/data/{id} |
Signed Token | Permanently remove chunk. |
POST |
/v1/data/{id}/replicate |
Signed Token | Trigger P2P replication to target nodes. |
Consistency Guarantees:
- Metadata: Linearizable (Raft).
-
Data: Sub-Quorum Persistent (
$R=3, W=2$ ). APUTreturns 201 Created once the local write and at least one remote replica succeed. Remaining replication completes in the background. - Write-Through: Metadata updates MUST only be submitted AFTER the Data sub-quorum is reached.
{
"id": "32_char_hex",
"links": {"parentID:nameHMAC": true},
"type": 0, // 0:File, 1:Dir, 2:Symlink
"owner_id": "user_id",
"group_id": "group_id",
"mode": 420, // octal 0644
"size": 1024,
"ctime": 1772517680144,
"nlink": 1,
"client_blob": "base64_enc_metadata",
"children": {"nameHMAC": {"id": "childID", "enc_name": "base64", "nonce": "base64"}},
"manifest": [{"id": "chunk_hash", "nodes": ["node_id"]}],
"chunk_pages": ["page_id"],
"lockbox": {"recipient_id": {"kem": "base64", "dem": "base64"}},
"version": 5,
"is_system": false,
"leases": {"nonce": {"id": "session_id", "exp": 123, "type": 1}},
"unlinked": false,
"user_sig": "base64_sig",
"group_sig": "base64_sig",
"owner_delegation_sig": "base64_sig",
"cluster_sig": "base64_sig"
}Clients MUST use this schema inside the encrypted client_blob:
{
"symlink_target": "path",
"inline_data": "base64",
"mtime": int64_ns,
"uid": uint32,
"gid": uint32
}{
"id": "derived_user_id",
"uid": 1001,
"sign_key": "base64_pub_dsa",
"enc_key": "base64_pub_kem",
"usage": {"inodes": 10, "bytes": 1048576},
"quota": {"max_inodes": 1000, "max_bytes": 1073741824}
}{
"id": "group_id",
"gid": 5001,
"owner_id": "owner_user_id",
"members": {"user_id": true},
"enc_key": "base64_pub_kem",
"sign_key": "base64_pub_dsa",
"lockbox": {"recipient_id": {"kem": "base64", "dem": "base64"}},
"version": 1,
"signer_id": "user_id",
"signature": "base64_sig",
"quota_enabled": true
}All multi-byte integers in hashes are Big-Endian.
The user_sig is over the SHA-256 hash of these fields concatenated exactly:
[]byte("id:" + id + "|")[]byte("v:")+BigEndian(uint64(version))+[]byte("|")[]byte("mode:")+BigEndian(uint32(mode))+[]byte("|")[]byte("gid_str:" + group_id + "|")[]byte("sys:" + (is_system ? "1" : "0") + "|")[]byte("client_blob:")+raw_encrypted_bytes+[]byte("|")[]byte("owner:" + owner_id + "|")[]byte("type:")+BigEndian(uint32(type))+[]byte("|")[]byte("links:")+SortedCSV(parentID:nameHMAC)+[]byte("|")[]byte("children:")+SortedCSV(nameHMAC:childID,hex_enc_name,hex_nonce|)// Each entry is separated by |[]byte("manifest:")+CSV(chunk_id)+[]byte("|")// Nodes are EXCLUDED from user-signed content hash.[]byte("pages:")+SortedCSV(chunk_page_ids)+[]byte("|")[]byte("lockbox:")+SortedCSV(id:kem+dem)+[]byte("|")
The group signature is over the SHA-256 hash of these fields concatenated exactly:
[]byte("DistFS-Group-v2|")[]byte("group-id:" + id + "|")[]byte("v:")+BigEndian(uint64(version))+[]byte("|")[]byte("nonce:")+nonce_bytes+[]byte("|")[]byte("sys:" + (is_system ? "1" : "0") + "|")[]byte("epoch:")+BigEndian(uint32(epoch))+[]byte("|")[]byte("enc_epoch_seed:")+enc_epoch_seed_bytes+[]byte("|")[]byte("client_blob:")+client_blob_bytes+[]byte("|")[]byte("owner:" + owner_id + "|")[]byte("signer:" + signer_id + "|")[]byte("anonymous_lockbox:")+CSV(ciphertext)+[]byte("|")[]byte("anonymous_registry:")+anonymous_registry_bytes+[]byte("|")[]byte("enc_key:")+enc_key_bytes+[]byte("|")[]byte("sign_key:")+sign_key_bytes+[]byte("|")[]byte("hsk:")+SortedCSV(epoch:sign_key)+[]byte("|")[]byte("enc_sign_key:")+enc_sign_key_bytes+[]byte("|")[]byte("lockbox:")+SortedCSV(id:kem+dem)+[]byte("|")[]byte("registry_lockbox:")+SortedCSV(id:kem+dem)+[]byte("|")[]byte("enc_registry:")+enc_registry_bytes+[]byte("|")[]byte("quota_enabled:" + (quota_enabled ? "1" : "0") + "|")
To hide filenames from the server, all map keys in children and links are HMACs:
nameHMAC = Hex(HMAC-SHA256(Parent_FileKey, plaintext_filename))
- Request:
{"inode_ids": ["..."], "type": 1, "duration": ns, "nonce": "..."} - Types:
0: Shared,1: Exclusive. - Rule: Mutations are rejected if an Exclusive lease is held by a different session.
- Request:
{"inode_id": "...", "chunks": ["hash"], "mode": "R|W|D"} - Response:
{"payload": "base64_minified_json", "sig": "base64_cluster_sig"} - Capability Schema:
{"chunks": ["hash"], "mode": "RWD", "exp": unix_ts}
The DistFS API maps FSM sentinel errors to structured JSON HTTP responses (APIErrorResponse). The following table defines the authoritative conditions under which each error MUST be triggered.
| Code | HTTP | FSM Sentinel | Intended Trigger Condition |
|---|---|---|---|
DISTFS_NOT_FOUND |
404 | ErrNotFound |
The requested Inode, User, or Group ID does not exist in the BoltDB state. |
DISTFS_EXISTS |
409 | ErrExists |
Attempting to create an Inode, User, or Group ID that already exists. |
DISTFS_VERSION_CONFLICT |
409 | ErrConflict |
Optimistic concurrency failure: the Version field in an update command does not strictly equal the current Version in the BoltDB state + 1. |
DISTFS_LEASE_REQUIRED |
409 | ErrLeaseRequired |
A mutation (update/delete) was attempted on an Inode, but the request's sessionID does not hold a valid, unexpired LeaseExclusive for that Inode. |
DISTFS_QUOTA_EXCEEDED |
403 | ErrQuotaExceeded |
The operation would cause the debtor's (User or Group) Usage to strictly exceed their Quota. Note: Groups with quota_enabled: true are always the debtors for their files. |
DISTFS_QUOTA_DISABLED |
403 | ErrQuotaDisabled |
Attempted to set a quota on a group that was created with quota_enabled: false. |
DISTFS_UNAUTHORIZED |
401 | (Auth Middleware) | The client failed to provide a valid signature, a valid session JWT, or the OIDC token is invalid/expired. |
DISTFS_FORBIDDEN |
403 | (ACL Checks) | The authenticated user is not the owner of the Inode/Group, is not an Admin, or is attempting a restricted action (e.g. promoting an admin). |
DISTFS_NOT_LEADER |
503 | raft.ErrNotLeader |
The HTTP request was routed to a node that is not the current Raft leader. Clients MUST retry against a different node. |
DISTFS_STRUCTURAL_INCONSISTENCY |
409 | ErrStructuralInconsistency |
An atomic batch mutation would result in a corrupted filesystem graph. Examples: a parent directory claims a child, but the child does not have a reciprocal links entry; an Inode is deleted but still has an active lease. |
DISTFS_ATOMIC_ROLLBACK |
500 / 409 | ErrAtomicRollback |
An explicitly marked atomic CmdBatch encountered a failure in one of its sub-commands (e.g., a version conflict). The FSM MUST trigger a BoltDB tx.Rollback() and return this error wrapping the original sub-command failure. |
DISTFS_INTERNAL_ERROR |
500 | (Various) | Unexpected errors, panics, cryptographic seal verification failures, or disk I/O errors. |
| Header | Usage | Effect |
|---|---|---|
Session-Token |
Mandatory for auth routes | Identifies the session and lease-owner. |
X-DistFS-Sealed |
Mandatory for mutations | Signals request body is a SealedRequest. |
X-DistFS-Admin-Bypass |
Optional (Admins only) | Bypasses ownership checks for recovery. |
X-DistFS-Sealed |
Optional for GET |
If true, server returns a SealedResponse. |