capsule/content/gemlog/an_overview_of_dory.gmi

64 lines
7.3 KiB
Text
Raw Normal View History

2023-05-27 11:21:11 -04:00
Meta(
title: "An overview of Dory",
summary: Some("An overview look at Dory, a Misfin protocol library in Rust"),
published: None,
tags: [
"misfin",
"rust",
"programming",
],
)
---
This is a follow up to my previous post about Dory.
=> gemini://gemini.hitchhiker-linux.org/gemlog/dory_a_misfin_protocol_library_in_rust.gmi Dory announcement
=> gemini://misfin.org/
So Dory is a WIP Misfin protocol library. That means that Dory will implement the Misfin spec without actually providing a server binary (although I will likely add a rudimentary single threaded example server). I'm writing it in a fairly generic way so that the library user can choose for themselves how to handle storing certificates and messages, as well as how to set up a listener. I have some opinions on those things, but I'll leave that for a later project when I actually put Dory into production.
Dory is written in Rust, as are most of my projects these days. I'm not going to argue with anyone about the merits of Rust, but it works well for me. I particularly love the tooling. I've been able to write a lot of tests as I go so I can be pretty sure that things are working as intended before actually trying it all out. I'm usually pretty careful about dependencies when using Cargo, however, as adding one line to Cargo.toml can easily bring in a half dozen or more transitive deps. Even so, some things don't make sense to implement completely from scratch, so I'm using the following libraries.
* rustls - managing tls connections
* digest and sha2 - generating certificate fingerprints
* time - parsing time into the correct format (this one I may change)
* x509-parser - parsing and validating certificates
* serde(optional) - serializing certain datatypes
2023-05-31 15:32:55 -04:00
The serde dep is behind a feature gate. I've put that in so that someone could provide, say, a json api for Android or IOS apps. There are likely a lot of other uses for having access to the server data in structured format.
## Certificate handling
First a little bit of background. Rust traits are a way of specifying common behaviors so that one can provide a more generic interface. Since Rust does not have inheritance, traits are the primary method of defining an interface while allowing for different implementations of that interface.
The `CertificateStore` trait is used to store server certificate fingerprints associated with the server's hostname. It has three methods.
* get_certificate
* insert_certificate
* contains_certificate
It should be obvious that this lines up pretty well with a dictionary or map like data structure, and indeed for convenience I've provided an implementation of the trait for both HashMap and BTreeMap data structures. That should be enough for a simple server for a single domain / single user setup, as both of those data structures can easily be stored in a text file. For a much larger setup one could write a database connector which fulfills the required methods and sub it in.
Unlike server certificates, when it comes to client certificates we need the full der encoded certificate and key. The `Certificate` struct holds that information, and is stored in a `ClientCertificateStore`, which is a generic storage trait like `CertificateStore`.
The Tofu verifier is a struct `Verifier` with a single field `store` which implements the rustls trait `ServerCertVerifier`. The `store` field is a mutext locked `CertificateStore`. Since that is the only piece of data required to initialize the verifier, you can create a verifier from a store by simple calling `store.into()`.
## Sending mail
A `Request` struct contains the message being sent along with the mailbox address of the sender. Since a single message can have multiple recipients the `Request::recipients()` method returns a list of the intended recipients, parsed from the message text recipients line.
The `Sender` struct has three fields, a `Request`, and both a `CertificateStore` and a `ClientCertificateStore`. Right now one creates a sender for each recipient of the message, but I intend to provide a method that loops through all of the intended recipients and sends a copy to each one. The sender is created with the `Sender::new` method which takes a request string and both stores as parameters. One then calls the `Request::send` method with the address of the intended recipient and the sender will open the connection, and create a verifier from the store and pass that to the rustls client config. If a client certificate for the sender is found in the client store it will pass that along to rustls as well. The sender then does the handshake and sends the message, returning the response from the server parsed into a `Response` struct.
Something which is subject to change is that right now Dory is only doing Tofu, ignoring the possibility that the server has a valid CA signed certificate. Indeed, if the server were to present the whole chain I'm pretty sure this would cause an error right now. I'm thinking I want to change this so that the first time a new server is encountered Dory will attempt CA verification and only if that fails fall back to Tofu.
## Receiving mail
This is basically just stubbed out right now, and a lot of it is probably going to be out of scope for the library anyway, as I want Dory to be pretty agnostic about how you want to set up your listener, whether that be a single threaded server, async or conventional multi-threaded model. The only parts that will be in scope are veryfying the client certificates and parsing the message as raw bytes, which will then be passed on to a generic storage backend.
## Mail storage
Like certificate storage, I've currently provided a generic `MailStore` trait which provides a number of methods for working with mail for a single domain.
* users - returns a list of users for that domain
* serves_domain - returns true if the server services the domain passed as an argument
* has_mailuser - ruturns true if the given username matches a valid account
* get_folder - returns a folder of messages if the named folder exists for the named user
* get_message - returns a message by the given title, if it exists
* delete_message - deletes the message if it exists, returns a boolean
* add_message - adds a message to the store in the given folder
* add_user - creates a new account
* remove_user - removes an entire user account
Right now I'm providing a `Domain` struct, which contains a hostname and a HashMap of accounts with their usernames as the keys. This would be usable for a single-user server with a limited number of messages all kept in memory and periodically synced to disk, but I'm thinking I also want to provide a filesystem backend where user accounts and folders correspond to actual filesystem folders and each message is stored in an individual text file. Such a storage scheme would be dead simple for a client to parse, and one could provide some rudimentary remote access using something like rsync, webdav or even git. Such a backend would also make it pretty simple to handle a multi-domain setup, as the existence (or lack thereof) of a subdirectory named for the domain would imply to the server whether it is to handle mail for the given domain. It would also be friendly to a gemini based webmail equivalent. The same client certificate used for sending Misfn mail could be used by the Gemini server to gate access to only the appropriate account folders.