In the previous post I've spent my time defining the necessary data structures for the chat program. In this post I will going to define the operations on it as it comes to my mind. This also will be the general draft of the API on the data. Here we will use Rust function syntax. Error codes/results are not shown for clarity. Many of these APIs will return a Rust Result of course.
The data is stored in an SQL database.
Therefore the structs mentioned in the previous post won't be actual objects we can put methods on.
Instead all the data is managed through a single database object.
Basically each structure in previous post will have a corresponding data table in the database.
Each object is identified by its primary key column in the corresponding table.
In this post the datatype that represents such key will be denoted as Id<T>.
The Vec<T> fields are omitted from the records in the database,
instead the table that contains the Ts will have a foreign key column that points back to the object that owns it.
For example ContactDeviceInfo would have a foreign key that points to a record in the Contacts table.
So using this foreign key, we can simply list the device infos that belong to the contact.
The Option types are simply nullable fields that may contain the value or key to the value or set NULL.
All the data tables support the creation, querying, listing, sorting, updating and deleting of records in them. I will omit the APIs whose sole purpose is adding, reading, modifying or deleting from a data table. I'll only mention the APIs that do a even a bit more of that.
So let's start:
UserData::new_primary
fn UserData::new_primary(
user_name: String,
primary_device_name: String,
server_name: Option<String>,
) -> UserData
This is going to be used to create a primary device. It creates the user CA and device keys and certificates. The user name and device name will be the subject names of the certificates. The server name is optional and is used for devices that are active and connectable.
UserData::new_secondary
fn UserData::new_secondary(
device_name: String,
server_name: Option<String>,
) -> UserData
This creates a secondary device that doesn't have the user CA key.
And it cannot have a device certificate either until the user CA issues it.
So neither the device_certificate nor the user_ca_certificate fields can be filled until a primary device signs it.
In this state the device is not in an usable state, so all APIs that require a working device will fail.
So now we need to change our structs already, this is how it would look:
struct SelfInfo {
dev_private_key: PrivateKey,
certificates: Option<DeviceCertificates>,
primary_device_info: Option<PrimaryDeviceInfo>,
access_methods: Vec<AccessMethod>,
user_metadata: UserMetadata,
historical_certificates: Vec<Certificate>, // for certificates that were used in past in this device.
last_update: Timestamp
}
struct DeviceCertificates {
device: Certificate,
user_ca: Certificate,
}
UserData::get_csr
fn UserData::get_csr(&self) -> CertificateSigningRequest
In order to make a secondary device work we need to obtain the certificate first. This API's purpose is to get a signing request, which can be processed by the primary device to issue the certificate. Calling this API doesn't make a lot of sense if the device is a primary device, because it has the CA key, but a secondary device can use it to request new certificates. Currently I don't know how this works in rcgen or whether it takes something extra (and even after more look I'm still unsure how to do it). So this part may change in the future. This certificate signing request needs to be serialized and somehow get to the primary device. The user interface of this thing needs to be designed.
UserData::sign_secondary_device
fn UserData::sign_secondary_device(&mut self, csr: CertificateSigningRequest) -> DeviceCertificates
This API is meant to be used on the primary device to create device certificates for a secondary device.
The resulting device certificates need to be sent back to the secondary device.
This also causes the new certificate to be added to the PrimaryDeviceInfo::certificates_signed list.
UserData::revoke_secondary_device
fn UserData::revoke_secondary_device(&mut self, cert: Certificate) -> CertificateRevocationList
If a secondary device is compromised, its certificate can be revoked.
This simply causes it to be added to the PrimaryDeviceInfo::certificates_revoked list.
And a CRL signed by the CA is returned, this needs to be moved to other secondary devices, so they can tell their contact lists to not trust the compromised device anymore.
This causes them to remove the corresponding ContactDeviceInfo entry if they have it and also take note of the revoked certificates.
So we will need a new field SelfInfo::revoked_certificates: Vec<CertificateRevocationList> field into the SelfInfo and use the lists there when making a connection or when verifying an incoming connection.
UserData::change_certificate
fn UserData::change_certificate(&mut self, dc: DeviceCertificates)
This is used to set or change the certificate for a secondary device. It's not meaningful for a primary device. After this the secondary device is functional.
Once the device is set up we need to add contact to start chatting. To do so, we need the user CA cert for the contact and a list of access methods using which the contact can be reached. Of course access methods are meaningful only for active nodes, passive nodes have no access method attached. So for this we need a new struct:
struct ContactCard {
user_ca: Certificate,
access_methods: Vec<AccessMethod>
}
Therefore the corresponding API might look like this:
fn UserData::add_contact(&mut self, card: ContactCard) -> Id<Contact>
So when a new contact is added, this struct is used and the appropriate structures are filled within the database.
Whenever you send or receive a message, the message will be added to the database.
One notable thing here is that for outgoing messages, when the other side confirms the receipt of a message Message::received_at field is set to the timestamp of the receipt.
The current device will try to retransmit the messages that aren't received yet.
SelfInfo
Setting the MOTD or changing the list of access methods also updates the timestamp of the info to the most recent timestamp.
This means the changes need to be propagated to the contacts so they can update the corresponding Contact records.
The ContactDeviceInfo::last_updated field contains a timestamp that says when was the last time we sent that device updates.
Once we contacted the device and sent all the things this device needs to know the field is updated to the current timestamp.
When a device has an access method, thus the device is an active node, then the current device will try to access that device regularly. Failed attempts and successes need to be kept track of. For this there are two APIs:
fn UserData::note_successful_connection(&mut self, state: Id<DeviceAccessState>);
fn UserData::note_failed_connection(&mut self, state: Id<DeviceAccessState>);
Successful connection will set the DeviceAccessState::number_of_failed_attempts to zero.
Failed attempt increases that field by one.
In both cases the DeviceAccessState::last_attempt field is set to the current timestamp.
So these are more interesting operations that come to my mind about what operations are on the data itself. It also highlighted that I need to experiment with a more things:
Previously I've used the WebPkiClientVerifier to verify client certificates on the server, it has the client side version called WebPkiServerVerifier that can be used to consider CRLs on the client side as well.
Okay, this took me way to much time to think through. That's it for now.
See the latest posts below, click the "..." to see them all. Click the tags to filter by tag. You can also subscribe to RSS in those lists.
Double entry bookkeeping explained - english finance
Chat2026 part 10: continuing application design - english chat2026-devblog
If you want privacy, please use a desktop PC - english privacy
YouTube is now practically unsearchable - english rants
Chat2026 part 9: using CRL in the server and client examples - english chat2026-devblog