Skip to content

Latest commit

 

History

History
714 lines (406 loc) · 29.6 KB

README.md

File metadata and controls

714 lines (406 loc) · 29.6 KB

tassis Documentation

Table of Contents

Overview

tassis provides back-end facilities for exchanging end-to-end encryptoed (E2EE) messages between messaging clients. It provides two key facilities:

  • Key Distribution
  • Message transport

These facilities are access via a websockets API

Key Distribution

Encryption is handled by clients using the Signal protocol.

tassis implements the "server" role as defined in Signal's Sesame paper.

With Signal, messages are actually exchanged between devices, with each device pair having its own crypto session. So for example, if user A wants to send a message to user B that can be read on all their devices, user A needs to establish independent sessions with all of user B's devices, independently encrypt the message and send the separate ciphertext to each device.

Each user's device is identified by an Address.

Signal clients use the X3DH key agreement protocol to establish encrypted session, which requires server-facilitiated exchange of key information.

tassis is ignorant of Signal's encryption algorithms and simply stores the information for use by Signal's client-side encryption logic as opaque data.

There are two ways that a session can be established:

  1. When a device wants to send an encrypted message to a user, it initiates one local session per recipient device. In order to do this, it needs to know the key information for the other devices, which it obtains by sending tassis a RequestPreKeys message and getting back one PreKey per known device. With this, it sends a session initiation message that contains all the key material that the recipient needs in order to establish a session on its end.

  2. When a device receives an encrypted session initiation message from another device, it sets up the encrypted session on its end without needing to retrieve any key material from tassis.

Message Transport

tassis facilities the exchange of messages between clients by supporting store-and-forward send and receive of opaque messages between clients. After connecting to tassis, clients can send messages to arbitrary addresses. Connected clients identify their own address upon connecting, at which point they can receive any messages that were sent to them.

Message Acknowledgement and Delivery Guarantees

Once clients have durably received a message, they acknowledge this to tassis, which in turn acknowledges receipt of the message to its broker so that the message won't be delivered in the future.

If acknowledgements are lost, messages may be delivered multiple times, so clients need to take care to deduplicate messages on their own end.

Message Retention

The messaging server enforces limits on how many messages are retained for each device. If devices do not retrieve messages prior to those limits being hit, messages may be lost in transit.

Websockets API

The public API uses websockets. Clients open two connections to ws[s]://server//, one authenticated connection and one unauthenticated connection. To preserve sender anonymity, they retrieve key material and send messages on the unauthenticated connection. All other operations happen on the authenticated connection.

All messages in the context of a client connection have a unique sequence number that identifies the message (separate sequences for both directions).

For all messages, if an error is encountered while processing the message, the server will respond with an Error whose sequence number is set to the sequence number of the message that led to the error.

For messages that require a response (like RequestPreKeys), if there was no error, the remote end will respond the corresponding response messages (like PreKey).

For all other messages, the remote end (both client and server) should respond with an Ack whose sequence number is set to the sequence number of the message that is being acknowledged.

Configuration Messages

Whenever a client opens a connection, the server immediately sends a Configuration message that informs the client of current configuration parameters like maximum attachment sizes, etc.

External Dependencies

tassis needs a database for storing key distribution information and a pub/sub message broker for exchanging messages between users.

Simple in-memory implementations of both are provided for testing. In production, we'll start with Redis based implementations. If performance of the message transport becomes an issue, we can consider something like Apache Pulsar.

tassis depends on Redis 6.2+. Currently, 6.2 is still in release candidate status. If tassis runs against an older version of Redis, it won't be able to delete acknowledged messages from the message queues. It will still bound message queues to a maximum length though, so it can be used in this way.

Security

Authentication

tassis supports both authenticated and unauthenticated connections. At the beginning of every connection, the server sends an AuthChallenge to the client with a nonce. Clients that wish to remain anonymous can simply ignore the challenge. Clients that wish to authenticate respond with an AuthResponse containing their Address (IdentityKey and DeviceId), the nonce from the challenge, and a signature over the Address+Nonce. The server then verifies that the nonce matches the expected value for this connection and that the signature is correct based on the sender's IdentityKey. If yes, the user is authenticated. If not, the server returns an error and closes the connection.

Key Distribution

In principle, tassis only provides a convenience for key-distribution, and it's encumbent on clients and end-users to verify key material for themselves. The identityKey is in fact also the public key corresponding to that identity (no key rotation allowed for a given identityKey), so in practice if people are confident that they're sending a message to the correct identityKey, they can be confident that it's being encrypted for reading by the owner of that IdentityKey.

Transport Security

Message Privacy and Integrity

Because clients use E2EE, tassis does not concern itself with protecting the contents of messages from eavesdropping or tampering

Sender Anonymity (Sealed Sender)

Sealed Sender is a scheme used by Signal for allowing senders to send messages without intermediaries know who sent the message. The original scheme involved the Signal server issuing a certificate attesting to the sender's identity and the sender encrypting that certificate and sending it to the recipient. Because tassis doesn't use phone numbers, it doesn't need to attest to the sender's identity. So here, the sender simply signs its own address information and encrypts that for transmission to the client. Because the address is encrypted just like with Signal, and clients send messages via unauthenticaated connections, tassis doesn't know the address of the sender, only the recipient. Our Java implementation of this can be found here.

Denial of Service

Because clients rely on tassis for the actual transport of messages, it is important to guard against various denial of services attacks. In addition to the typical denial of service attacks faced by any web service, tassis guards against the following categories of attack:

Message Flooding

Rate limiting should be used to prevent individual clients from flooding the network, or any particular user, with messages. this is not yet implemented

Message Stealing

In order to prevent unauthorized users from stealing messages before they can be received by their legitimate clients, tassis authenticates clients based on IdentityKey to make sure that only authorized clients may read messages on behalf of a specific user.

Chat Numbers

Tassis assigns every IdentityKey a unique ChatNumber, which is a number that looks like a phone number but isn't. Tassis communicates the IdentityKey's ChatNumber in its response to successful authentications.

Full ChatNumber

The full ChatNumber is an encoding of the IdentityKey.

Short ChatNumber

The short ChartNumber is a prefix of the full ChatNumber which can be used to look up the full ChatNumber.

Avoiding Collisions

It is possible for two distinct IdentityKeys to encode to the same ChatNumber, which would cause a collision. In order to distinguish these identities, tassis adds 5s to the start and end of the short ChatNumber for whichever identity came second.

For example, let's say we have the following two full chat numbers

  • 292013940138492304132429201394013849230413242920139401384923041324292013940138491
  • 292013940138492304132429201394013849230413242920139401384923041324292013940138492

The first gets a standard short number:

292013940138492304132429201394013849230413242920139401384923041324292013940138491 -> 292013940138

The second gets some 5's inserted into the full and short numbers to distinguish it:

52920139401385492304132429201394013849230413242920139401384923041324292013940138492 -> 52920139401385

When parsing ChatNumbers, 5s are ignored, so the modified form of the above full ChatNumber still parses into the same IdentityKey as the original form.

The reason for modifying the full number in addition to the short number is that this enforces the invariant that the short chat number should always be a prefix of the full number.

Rate limiting

There is only a limited amount of short ChatNumbers available, specifically 125,524,238,436 (125 billion). In order to avoid malicious actors consuming and exhausting this pool, tassis imposes a global rate limit of approximately 2 chat number registrations per second. At that rate, tassis will allow up to 63,072,000 short ChatNumbers to be registered per year, which is only 0.05% of the total available pool.

Message Exchange Flow

Message Exchange Flow

Messages

Top

model/Messages.proto

tassis uses an asynchronous messaging pattern for interacting with the API.

Clients typically connect to tassis via WebSockets to exchange messages.

Clients will typically open two separate connections, authenticating on one and leaving the other unauthenticated.

The unauthenticated connection is used for retrieving other identities' preKeys and sending messages to them, so as not to reveal the identityKey of senders.

The authenticated connection is used for all other operations, including performing key management and receiving messages from other identities.

Authentication is performed using a challenge-response pattern in which the server sends an authentication challenge to the client and the client responds with a signed authentication response identifying its identityKey and deviceId. On anonymous connections, clients simply ignore the authentication challenge. Successful authentications are acknowledged with a Number that gives information about the IdentityKey number under which the authenticated user is registered. This number is constant over time.

Messages sent from clients to servers follow a request/response pattern. The server will always respond to these with either an Ack or a typed response. In the event of an error, it will respond with an Error message. This includes the following messages:

  • Register -> Ack
  • Unregister -> Ack
  • RequestPreKeys -> PreKeys
  • OutboundMessage -> Ack

Some messages sent from the server to the client require an Ack in response:

  • inboundMessage -> Ack

Some messages don't require any response:

  • PreKeysLow

All messages sent within a given connection are identified by a unique sequence number (separate sequences for each direction). When a response message is sent in either direction, its sequence number is set to the message that triggered the response so that the client or server can correlate responses with requests.

Ack

Acknowledges successful receipt of a Message

Address

An Address for a specific client

Field Type Label Description
identityKey bytes The 32 byte ed25519 public key that uniquely identifies an identity (e.g. a user)
deviceId bytes Identifier for a specific device, only unique for a given identityKey

AuthChallenge

A challenge to the client to authenticate. This is sent by the server once and only once, immediately after clients connect.

Field Type Label Description
nonce bytes A nonce to identify this authentication exchange

AuthResponse

A response to an AuthChallenge that is sent from the client to the server on any connection that the client wishes to authenticate. The server will accept an AuthResponse only once per connection.

Field Type Label Description
login bytes The serialized form of the Login message
signature bytes A signature of the serialized Login message calculated using the private key corresponding to the IdentityKey that's logging in

ChatNumber

A number representing the IdentityKey in this system.

Field Type Label Description
number string a form of IdentityKey that looks like a phone number
shortNumber string short version of the number
domain string the domain within which the short number is registered

Configuration

Provides configuration information to clients

Field Type Label Description
maxAttachmentSize int64 The maximum allowed attachment size (encrypted size, not plaintext)

Error

Indicates that an error occurred processing a request.

Field Type Label Description
name string An identifier for the error, like "unknown_identity"
description string Optional additional information about the error

FindChatNumberByIdentityKey

Requires anonymous connection

A request to look up a ChatNumber corresponding to an IdentityKey.

Field Type Label Description
identityKey bytes the identity key for which to look up the ChatNumber

FindChatNumberByShortNumber

Requires anonymous connection

A request to look up a ChatNumber corresponding to a short number.

Field Type Label Description
shortNumber string the short number for which to look up the ChatNumber

ForwardedMessage

Used internally by tassis for messages that are to be forwarded to a federated tassis

Field Type Label Description
message OutboundMessage The message that's being forwarded
firstFailed int64 The unix timestamp in milliseconds for when the message first failed to forward
lastFailed int64 The unix timestamp in milliseconds for when the message most recently failed to forward

InboundMessage

An inbound message from another client to the currently authenticated client

Field Type Label Description
unidentifiedSenderMessage bytes A sealed sender message (opaque to tassis).

Login

Login information supplied by clients in response to an AuthChallenge.

Field Type Label Description
address Address The Address that's logging in. This will become permanently associated with the current connection
nonce bytes This echoes back the nonce provided by the server in the AuthChallenge

Message

The envelope for all messages sent to/from clients.

Field Type Label Description
sequence uint32 the message sequence, either a unique number for request messages or the number of the request message to which a response message corresponds
ack Ack
error Error
configuration Configuration
authChallenge AuthChallenge
authResponse AuthResponse
register Register
unregister Unregister
requestPreKeys RequestPreKeys
preKeys PreKeys
preKeysLow PreKeysLow
requestUploadAuthorizations RequestUploadAuthorizations
uploadAuthorizations UploadAuthorizations
outboundMessage OutboundMessage
inboundMessage InboundMessage
findChatNumberByShortNumber FindChatNumberByShortNumber
findChatNumberByIdentityKey FindChatNumberByIdentityKey
chatNumber ChatNumber

OutboundMessage

Requires anonymous connection

A message from one client to another.

Field Type Label Description
to Address The Address of the message recipient
unidentifiedSenderMessage bytes A sealed sender message (opaque to tassis). This is what will be delivered to the recipient.

PreKey

Information about a PreKey for a specific Address.

Field Type Label Description
deviceId bytes The deviceId that this key material belongs to
signedPreKey bytes The most recent signedPreKey for the device at this Address. See https://crypto.stackexchange.com/questions/72148/signal-protocol-how-is-signed-preKey-created
oneTimePreKey bytes One disposable preKey for the device at this Address. May be empty if none were available (that's okay, Signal can still do an X3DH key agreement without it).

PreKeys

A list of PreKeys for all of an identityKey's devices, sent in response to RequestPreKeys

Field Type Label Description
preKeys PreKey repeated One or more preKeys

PreKeysLow

A notification from the server to the client that we're running low on oneTimePreKeys for the Address associated to this connection.

Clients may choose to respond to this by sending a Register message with some more preKeys. This does not have to be tied to the initial PreKeysLow message.

Field Type Label Description
keysRequested uint32 The number of additional oneTimePreKeys that the server is requesting.

Register

Requires authentication

A request to register a signed preKey and some set of one-time use preKeys. PreKeys are used by clients to perform X3DH key agreement in order to establish end-to-end encrypted sessions.

This information is registered in the database under the client's Address. If multiple registrations are received, if signedPreKey matches the information on file, the new preKeys will be appended to the ones already on file. Otherwise, the existing registration will be replaced by the latest.

Field Type Label Description
signedPreKey bytes The signedPreKey for this device.
oneTimePreKeys bytes repeated Zero, one or more disposable preKeys for this device.

RequestPreKeys

Requires anonymous connection

A request to retrieve preKey information for all registered devices for the given identityKey except those listed in knownDeviceIds.

Field Type Label Description
identityKey bytes The identityKey for which to retrieve preKeys.
knownDeviceIds bytes repeated Devices of this identity which the client already knows about and doesn't need preKeys for.

RequestUploadAuthorizations

Requests attachment upload authorizations.

Field Type Label Description
numRequested int32 the number of authorizations requested. The server may not return the number requested.

Unregister

Requires authentication

Removes the recorded registration for the client's Address.

UploadAuthorization

Provides authorization to upload an attachment to cloud storage

Field Type Label Description
uploadURL string The URL to which to upload
uploadFormData UploadAuthorization.UploadFormDataEntry repeated This form data needs to be included with the upload in order to authorize it
authorizationExpiresAt int64 The unix timestamp in milliseconds when this authorization expires and can no longer be used
maxUploadSize int64 The maxmimum number of bytes that are allowed to be uploaded
downloadURL string The URL from which the attachment may be downloaded once it has been uploaded

UploadAuthorization.UploadFormDataEntry

Field Type Label Description
key string
value string

UploadAuthorizations

Multiple UploadAuthorizations

Field Type Label Description
authorizations UploadAuthorization repeated

Scalar Value Types

.proto Type Notes C++ Java Python Go C# PHP Ruby
double double double float float64 double float Float
float float float float float32 float float Float
int32 Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. int32 int int int32 int integer Bignum or Fixnum (as required)
int64 Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. int64 long int/long int64 long integer/string Bignum
uint32 Uses variable-length encoding. uint32 int int/long uint32 uint integer Bignum or Fixnum (as required)
uint64 Uses variable-length encoding. uint64 long int/long uint64 ulong integer/string Bignum or Fixnum (as required)
sint32 Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. int32 int int int32 int integer Bignum or Fixnum (as required)
sint64 Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. int64 long int/long int64 long integer/string Bignum
fixed32 Always four bytes. More efficient than uint32 if values are often greater than 2^28. uint32 int int uint32 uint integer Bignum or Fixnum (as required)
fixed64 Always eight bytes. More efficient than uint64 if values are often greater than 2^56. uint64 long int/long uint64 ulong integer/string Bignum
sfixed32 Always four bytes. int32 int int int32 int integer Bignum or Fixnum (as required)
sfixed64 Always eight bytes. int64 long int/long int64 long integer/string Bignum
bool bool boolean boolean bool bool boolean TrueClass/FalseClass
string A string must always contain UTF-8 encoded or 7-bit ASCII text. string String str/unicode string string string String (UTF-8)
bytes May contain any arbitrary sequence of bytes. string ByteString str []byte ByteString string String (ASCII-8BIT)