README.md

Constellation

Constellation is a self-managing, peer-to-peer system in which each
node:

Hosts a number of NaCl (Curve25519) public/private key pairs.

Automatically discovers other nodes on the network after
synchronizing with as little as one other host.

Synchronizes a directory of public keys mapped to recipient hosts
with other nodes on the network.

Exposes a public API which allows other nodes to send encrypted
bytestrings to your node, and to synchronize, retrieving
information about the nodes that your node knows about.

Exposes a private API which:

Allows you to send a bytestring to one or more public keys,
returning a content-addressable identifier. This bytestring is
encrypted transparently and efficiently (at symmetric
encryption speeds) before being transmitted over the wire to
the correct recipient nodes (and only those nodes.) The
identifier is a hash digest of the encrypted payload that
every receipient node receives. Each recipient node also
receives a small blob encrypted for their public key which
contains the Master Key for the encrypted payload.

Allows you to receive a decrypted bytestring
based on an identifier. Payloads which your node has sent or
received can be decrypted and retrieved in this way.

Exposes methods for deletion, resynchronization, and other
management functions.

Supports a number of storage backends including LevelDB,
BerkeleyDB, SQLite, and Directory/Maildir-style file storage
suitable for use with any FUSE adapter, e.g. for AWS S3.

Uses mutually-authenticated TLS with modern settings and various trust
models including hybrid CA/tofu (default), tofu (think OpenSSH), and
whitelist (only some set of public keys can connect.)

Supports access controls like an IP whitelist.

Conceptually, one can think of Constellation as an amalgamation of a
distributed key server, PGP encryption (using modern cryptography,)
and Mail Transfer Agents (MTAs.)

Constellation's current primary application is to implement the
"privacy engine" of Quorum, a fork of Ethereum with support for
private transactions that function exactly as described in this
README. Private transactions in Quorum contain only a flag indicating
that they're private and the content-addressable identifier described
here.

Constellation can be run stand-alone as a daemon via
constellation-node, or imported as a Haskell library, which allows
you to implement custom storage and encryption logic.

Downloading precompiled binaries

Constellation binaries for most major platforms can be downloaded here.

Installation from source

First time only: Install Stack:

Linux: curl -sSL https://get.haskellstack.org/ | sh

MacOS: brew install haskell-stack

First time only: run stack setup to install GHC, the Glasgow
Haskell Compiler

Run stack install

Generating keys

To generate a key pair "node", run constellation-node --generatekeys=node

If you choose to lock the keys with a password, they will be encrypted using
a master key derived from the password using Argon2id. This is designed to be
a very expensive operation to deter password cracking efforts. When
constellation encounters a locked key, it will prompt for a password after
which the decrypted key will live in memory until the process ends.

Configuration File Format

How It Works

Each Constellation node hosts some number of key pairs, and advertises
a publicly accessible FQDN/port for other hosts to connect to.

Nodes can be started with a reference to existing nodes on the network
(with the othernodes configuration variable,) or without, in which
case some other node must later be pointed to this node to achieve
synchronization.

When a node starts up, it will reach out to each node in othernodes,
and learn about the public keys they host, as well as other nodes in
the network. In short order, the node's public key directory will be
the same as that of all other nodes, and you can start addressing
messages to any of the known public keys.

This is what happens when you use the send function of the Private
API to send the bytestring foo to the public key
ROAZBWtSacxXQrOe3FGAqJDyJjFePR5ce4TSIzmJ0Bc=:

The local node encrypts the payload using NaCl secretbox using
the random MK and nonce.

The local node generates an MK container for each recipient
public key; in this case, simply one container for ROAZ...,
using NaCl box and the recipient nonce.

NaCl box works by deriving a shared key based
on your private key and the recipient's public key. This is known
as elliptic curve key agreement.

Note that the sender public key and recipient public key we
specified above aren't enough to perform the
encryption. Therefore, the node will check to see that it is
actually hosting the private key that corresponds to the given
public key before generating an MK container for each recipient
based on SharedKey(yourprivatekey, recipientpublickey) and the
recipient nonce.

We now have:

An encrypted payload which is foo encrypted with the random
MK and a random nonce. This is the same for all recipients.

A random recipient nonce that also is the same for all
recipients.

For each recipient, the MK encrypted with the
shared key of your private key and their public key. This
MK container is unique per recipient, and is only transmitted to
that recipient.

For each recipient, the local node looks up the recipient host,
and transmits to it:

The sender's (your) public key

The encrypted payload and nonce

The MK container for that recipient and the recipient nonce

The recipient node returns a SHA3-512 hash digest of the
encrypted payload, which represents its storage address.

(Note that it is not possible for the sender to dictate the
storage address. Every node generates it independently by hashing
the encrypted payload.)

The local node stores the payload locally, generating the same
hash digest.

The API call returns successfully once all nodes have confirmed
receipt and storage of the payload, and returned a hash digest.

Now, through some other mechanism, you'll inform the recipient that
they have a payload waiting for them with the identifier owqkrokwr,
and they will make a call to the receive method of their Private
API:

Make a call to the Private API socket receive method:
{"key": "qrqwrqwr"}

The local node will look in its storage for the key qrqwrqwr,
and abort if it isn't found.

When found, the node will use the information about the sender as
well as its private key to derive SharedKey(senderpublickey,
yourprivatekey) and decrypt the MK container using NaCl box
with the recipient nonce.

Using the decrypted MK, the local node will decrypt the encrypted
payload using NaCl secretbox using the main nonce.