Workspace Signature Chain
In order to determine the list of active team members a chain of signed events is used. In addition they can share encrypted data with each other without the central service being able to read it.
There is a prototype available here (opens in a new tab), but yet it's not implemented in the application.
Goal
The project is inspired by web of trust and blockchains to aim for the following behaviour:
- Enable asynchronous exchange of data (participants don't have to be online at the same time)
- Only one verification is necessary to establish trust between everyone in the workspace
- Workspace access to a member can be revoked instantly
- A workspace can't be manipulated in hindsight
- "Efficiently" be able download the current state by a client (e.g. not running a full blockchain node)
High Level Architecture
To achieve the defined goals the signature chain relies on a central service in combination with asymmetric cryptography.
The current state of a workspace can be constructed from a series of events (chain) and multiple encrypted state entries (encrypted state).
Chain
The purpose of the chain is determine who is part of the organization and what permissions does this member have.
The central service as well as the members must have full access to this information.
EncryptedState
The purpose of the encrypted-state is contain information like usernames to hide away from the central service. Only members have access to the content of the encrypted state.
Design Decisions
Why a central server?
In decentralized systems there are two issues that didn't align with the goals because:
- A change e.g. adding or removing a participant from a group needs to propagate to all participants before the take effect and therefor state is only eventual consistent (opens in a new tab).
- Afaik asynchronous exchange and strong consistency is conflicting. For example blockchains require a lot of online nodes to verify the chain and to achieve a certain gurantee that the current version is the one that will be used and not another fork. There are some ideas and proposal how to tackle it though e.g. local-first-web/auth discussion (opens in a new tab)
The three main objectives for the server are:
- Be an always online instance for members to asynchronously exchange data
- Prevent members to submit updates based on outdated state which ensures a correct data integrity (for the unencrypted state).
- Instantly revoke access to removed members.
How does it work?
For example the encrypted state has a logical clock in the public, but authenticated data submitted by a user. This way the server can throw an error in case the user didn't have the latest state. The user's client can then fetch the latest information and retry.
Note: Depending on the UX the user might want to re-review their changes. This might vary from case to case.
Known Attack Vectors
- Member
- Admin
- Server
- Member collaborating with the central service
- Admin collaborating with the central service
Meta Data
Since the chain is public all the meta data about who has access to the group and all their permissions are visible to the central service. This is a known trade-off and possibly an evolution of the protocol using zero knowledge proofs (like the Signal Private Group System (opens in a new tab)) could reduce the meta data visible to the server while keep the functionality.
Security Properties
- Forward Secrecy (opens in a new tab) is currently not supported. A future evolution of the project ideally uses a ratchet (opens in a new tab) to enable forward secrecy. Inspirations could be DCGKA (opens in a new tab).
- Post-compromise security (opens in a new tab) is currently not supported. Only when a member gets removed the synchronous encryption key and related lockboxes are replaced which leads to PCS in this case.
Trust Chain Package
Trust Chain Events
-
create-chain
- Usually created by one author, but can be more as well.
-
add-member
- Members can only add another member if they have the permission
canAddMember
. A member can not add another member as admin. - Admins can add members and set the permissions
canAddMember
andcanRemoveMember
.
- Members can only add another member if they have the permission
-
remove-member
- Members can only remove another member if they have the permission
canAddMember
. A member can not remove another member as admin. - Admins can add members and set the permissions
canAddMember
andcanRemoveMember
.
- Members can only remove another member if they have the permission
-
update-member
- Only admins can update the authorization info
isAdmin
,canAddMember
andcanRemoveMember
of other members.
- Only admins can update the authorization info
EncryptedState Structure
Example structure of the encrypted state:
[
{
// identified for symmetric key that's used
"keyId": "43b89b1c-6c7b-48a6-839c-20cc495d3f97",
"ciphertext": "yYV1_uUtoFPRBRBCUtyv-jwPLfQI2UFYMovnUUFJh5UXALMkq69KM73YI3WaqZGrcclwTE7jMYAsYKR5rU0d-Q7yprXOS_1hXZ8TeKyQ-vSxpgZmgJVUK5zP_HDyFB4ackgBRdws6IyOMxc1Kz_HB-SCqC8Vk26H7YhGaIQWs4MXNpqgTg17cIo7Q2TL2shYN_VHSHmAeojByOfyDpUP3kpXK3zduIZZEKG3tuwjnzN0JG83BCPel8Iyrh91_UnBpctnXQUEhkoQ_ZJ_6YXpGYTnhTFur40NhX5nDjpIImbCiMoVAVIU_KZEcQ4bzGZ1_eM8AeRKTiie",
"nonce": "YOnlwPnEtwnfEQa0nEQR41RvQ7Dxt2NB",
// global logical clock to order the encrypted states (to ensure deterministic data resolving)
"publicData": "{\"clock\":2}",
"author": {
"publicKey": "pkcVysaH_mC-TpXzZEAAeB47rIqsWwubaM4stZQu-B4",
"signature": "XTNuX2mqcpRF4nLnhHURAYblpE68ftPsaNW3ZsdZY1Se7BlLTjJSnADrV9Rn4AhM4MIjAsU8mj003vQCJUtjAA"
},
"lockbox": {
"keyId": "43b89b1c-6c7b-48a6-839c-20cc495d3f97",
"receiverSigningPublicKey": "pkcVysaH_mC-TpXzZEAAeB47rIqsWwubaM4stZQu-B4",
"senderLockboxPublicKey": "7CVtpWbqKFJvZO7hso3dOmrrwva_3uDgm3erquuTFRQ",
"ciphertext": "f3_AVc5xIxC8ejhBflv_qhAjtZoTveMroA9H4uATn51yKKpToegWn_dB-P-oYG8IK_CKDt4mfmGD-43M5KVSeAykPDiPIxAIBAgNZfjVJMwhiYUFMt4U26WTlHX4aBFa75-VztyF2TcYmkhEbFw-Gf0fVVs",
"nonce": "HLDYyAc4GMEcCqHSE2a_N-l1oaRkWfCL"
}
}
]
Error Philosophy
The chain will not accept any invalid input. It will not ignore them, but rather throw an Error. When creating an event there is no validation.
Here an example for clarification:
const event = createChain(…); // does not throw errors
const state = resolveState([event]); // throws errors (internally uses applyEvent)
const event2 = addMember(…); // does not throw errors
const newState = applyEvent(state, event2); // throws errors
Known Issues
- Remove member (based on a event proposal does not work)
- Chain/Encryped State related
- when promoting someone to an admin then set the name in the encrypted state
- when removing a member, take over the encrypted state updates from them
- when removing permissions from a member, take over the encrypted state updates
- Admin actions need to be in sync, meaning I can't vote on two admin interaction at the same time. Any additional chain event will invalidate them. See future improvements for a possible solution.
Future Improvements
- Exhaustive TS Matching
- Functions should not mutate incoming parameters