dmcn protocol GitHub Read the spec
Spec Protocol Try it FAQ Go module
GitHub
On this page

How the protocol works

Overview#

DMCNP is seven layers. Speak all of them and you interoperate with the network.

Layer What it does
User identity An Ed25519 signing key and an X25519 key-exchange key. The address is local@domain, and its record signs itself.
Resolution A _dmcn.<domain> TXT record gives you a fingerprint to trust and a few nodes to dial. You fetch signed records from the nodes it names and check them against the fingerprint.
Message model PlaintextMessage, then SignedMessage, then EncryptedEnvelope. One AES-256-GCM key per message, wrapped to each recipient over X25519. Header and body are sealed separately, and both are padded to fixed size classes.
Routing RelayHints say which relays hold a mailbox. They sit outside the owner's signature, so an operator can move a mailbox without the owner's key, and the address never changes.
Relay service /dmcn/relay/1.0.0: store, fetch, mailbox operations, record lookups and onion forwarding, as length-prefixed protobuf over libp2p.
Trust and federation Each domain has an authority record, anchored in DNS, that delegates to issuers. Peers exchange and verify credentials at /dmcn/join before they federate.
Transport libp2p streams. Discovery is seeded from DNS, with no DHT, on purpose.

The protocol itself is four .proto files, defining identity records, credentials, the message envelope and the relay wire format. They're the contract. If the spec and the schema ever disagree, the schema wins, and the reference further down this page is generated from it.

Why there's no global directory#

Most decentralised messaging puts identity in a shared overlay: a DHT, a chain, a consensus set. DMCNP doesn't, and the reason is practical. A big enough hostile majority in a shared overlay can quietly withhold records, and for something meant to replace email that's fatal.

So resolution works the way mail delivery already does. A domain publishes a _dmcn TXT record with its trust anchor and a few seed nodes. You read it, dial the nodes it names, fetch the signed record, and check it against the anchor from DNS.

A domain is served by the nodes its own DNS names: its own, or a host it explicitly delegates to. It's never served by a shared pool it didn't choose. Records sign themselves, so a server that isn't your domain's authority can refuse to answer you, but it can't lie to you.

Core and extensions#

The core is what you need to interoperate: resolve an address, verify an identity, send mail and receive it. Two capabilities are optional and can be skipped entirely: onion routing, and an SMTP bridge to ordinary email.

Anything an operator wants but the network doesn't need is an extension: fleet administration, hosting permits, entitlements, quotas. Extensions attach through surfaces the core sets aside for them, never through new core fields.

The rule that split protects: ignore every extension and you still interoperate. An extension can give an operator new powers. It can never make your implementation stop working with the network. Retired field numbers stay reserved forever.

Status#

This is a snapshot of the reference implementation, not a frozen standard. The schema moves with the implementation, and where this site and the implementation disagree, the implementation wins. Formal versioning and a conformance suite are on the roadmap and aren't done yet.

The wire schema is the compatibility contract. Everything under internal/ in the repository is how one implementation happens to work, and carries no stability promise.

Licence#

Apache-2.0 covers the code and the schema, with an express patent grant. It doesn't cover the names: implement the protocol under whatever name you like, but don't call something DMCNP unless it really conforms. The FAQ has the detail.

Identity and addressing#

An address such as alice@example.com is a keypair: an Ed25519 signing key and an X25519 key-agreement key, bound in a self-certifying IdentityRecord that any reader can verify on its own. No certificate authority issues it.

The record has two ownership zones. The identity core (address, keys, validity, tier and revision) is covered by the owner's self-signature, so only the key holder can change it. The operational side is mailbox routing. It travels as an operator-signed credential that is deliberately excluded from the self-signature, so a domain operator can re-point or rebalance routing without ever holding the owner's key. That credential verifies against the domain authority instead (see Credentials and authority).

The owner-signed revision never goes down. Fleets and readers reject any record whose revision is lower than one they've already verified, so a replayed old record can never roll an identity back.

IdentityRecord fields
#fieldtypenotes
1versionuint32record format version
2addressstringlocal@domain
3ed25519_public_keybytessigning key (32 bytes)
4x25519_public_keybyteskey-agreement key (32 bytes)
5created_atint64unix seconds
6expires_atint64unix seconds; 0 = no expiry
7relay_hintsrepeated stringreader-facing mirror only, not covered by the self-signature; the authoritative copy lives inside routing_credential (25)
8verification_tierVerificationTiertrust tier (see the tier table); enforcement is reader-side
9attestationsrepeated AttestationRecordweb-of-trust attestations (in-person, fingerprint, network, organizational)
10self_signaturebytesowner Ed25519 (64 bytes) over fields 1–6, 8, 23, 26, 29, 30; ctx dmcn-identity-self-v1\0
11–22(reserved)—do not reuse
23require_onionboolmailbox requires onion delivery, so relays reject direct stores; covered by the self-signature
24address_credentialCredentialthe domain's attestation of the address↔key binding (role address); issued after self-signing, excluded from the self-signature
25routing_credentialCredentialoperator-owned routing (role routing) carrying the authoritative relay_hints; excluded from the self-signature so operators can re-point routing without the owner's key
26revisionuint64monotonic and owner-signed, so a lower revision can never overwrite a higher one (anti-rollback)
27(reserved)—do not reuse
28operator_credentialsrepeated Credentialgeneric operator extension point: operator-attached credentials beyond routing (semantics by role/attributes); excluded from the self-signature; anti-rollback tiebreaks on the newest issued_at across 25 and these
29rotation_chainrepeated RotationEntrythe address's own key-change history: one owner-authorized transition per entry, each signed BOTH by the key it retires and by the key taking over; covered by the self-signature; capped, with the complete history in the address's AddressHistoryRecord
30recovery_ed25519_public_keybytesowner-held key, kept apart from the active one, that may authorize the next rotation when the active key is lost; covered by the self-signature; empty when none is enrolled
Verification tiers
valuenamemeaning
0VERIFICATION_TIER_UNVERIFIEDvalid but untrusted; addresses register here and still work, and trust is upgraded, never gated at registration
1(reserved)do not reuse
2VERIFICATION_TIER_DOMAIN_DNSraised by a domain attestation (address credential) chained to the DNS-anchored domain authority
3VERIFICATION_TIER_DANEreserved highest tier: DNSSEC/DANE-anchored

Resolving an address#

The record format, and what a resolver checks. The design behind it is under Why there's no global directory.

_dmcn.<domain>  TXT  "dmcn-verification=v1; fp=<40-hex>[; fleet=<domain>][; seed=<multiaddr>]..."

fp     trust anchor: first 20 bytes of SHA-256(ed25519_pub ‖ x25519_pub), uppercase hex
fleet  optional deferral to a hosting fleet's domain, for discovery only; spoofing it is DoS, never forgery
seed   bootstrap multiaddrs; each ends /p2p/<peerID>, so the transport handshake authenticates the endpoint
$ dig +short TXT _dmcn.example.com
"dmcn-verification=v1; fp=9F2A…C41E; seed=/dns4/id1.example.com/tcp/4001/p2p/12D3Koo…"

A resolver dials a seed, fetches the domain authority record and the identity record, and verifies them in this order:

  1. the domain authority record's root self-signature is valid;
  2. the authority's fingerprint equals the DNS fp= anchor (fail-closed);
  3. if the authority declares a fleet domain, it matches the DNS fleet= deferral (opt-in pinning);
  4. the identity record's owner self-signature is valid;
  5. the address credential chains to the domain authority (Credentials and authority);
  6. neither key is blocked and the address is not tombstoned.

Failures are strict on purpose. No _dmcn record means the address doesn't exist, and a transient DNS failure fails closed rather than downgrading trust.

Messages and encryption#

A message is written in plaintext, signed with the sender's key, then sealed once with a fresh per-message key that's wrapped separately for each recipient. That makes three layers: PlaintextMessage → SignedMessage → EncryptedEnvelope.

Envelope version 2 seals the content in two parts. The header is small and listable: sender, subject, snippet, recipient lists, and signed commitments to the body. The body holds the content and attachments and is fetched separately, so a mailbox can list mail without pulling bodies. The bcc list appears only on the sender's own Sent copy. Recipient copies carry an empty one.

cek = random 32 bytes                              # one per message
for each recipient device:
    eph         = fresh X25519 keypair
    shared      = X25519(eph_priv, recipient_pub)
    kwk         = HKDF-SHA256(shared, salt="", info=<context>)
    wrapped_cek = AES-256-GCM(kwk, cek)            # nonce 12, tag 16
content = AES-256-GCM(cek, plaintext, aad)         # header and body sealed separately in v2

# each wrap declares its own derivation in recipient_record.kdf; absent means 1
kdf=1: info = "dmcn-cek-wrap-v1"                              # bare label, binds nothing
kdf=2: info = "dmcn-cek-wrap-v2" ‖ eph_pub ‖ recipient_pub    # RFC 9180 §4.1 kem_context

# header and body share one CEK, so each seal binds a label to keep them apart.
# the labels are selected by the same kdf value, so blobs and wraps cannot disagree.
kdf=1: aad = none
kdf=2: aad = "dmcn-aad-hdr-v1\0"  |  "dmcn-aad-body-v1\0"

A reader dispatches on the declared value and never tries derivations in turn. The field sits on the recipient record rather than the envelope because the record is what survives storage. A mailbox keeps per-recipient entries and drops the envelope's version, message id and timestamp.

A reader must also check that the sender addressed the message to the mailbox reading it, meaning some address in the signed audience (recipient_address, to, cc) resolves to the key the envelope was sealed to. That check, not the encryption, is what catches a message re-targeted at someone it wasn't addressed to. A legitimate recipient holds the message key, so it can re-seal the same signed header to a third party, but it can't change the audience the sender signed. The comparison is on the key rather than the address text, so several addresses sharing one mailbox need no special case.

Sealed blobs are padded into standard size buckets so ciphertext length gives away as little as possible. The padded layout is [4-byte BE length][payload][zero padding]. The body blob also carries a cleartext content address that any relay can verify without a key:

body_content_address = 0x01 0x55 0x12 0x20 ‖ SHA-256(body_nonce ‖ encrypted_body ‖ body_tag)
                       CIDv1(raw, sha2-256), 36 bytes; the signed copy inside the header is authoritative

v1 gives at-rest and in-transit confidentiality with per-message keys, but not full forward secrecy. The ratchet_pub_key field is reserved for a double-ratchet upgrade and is zero today.

EncryptedEnvelope fields
#fieldtypenotes
1versionuint321 = single-blob payload; 2 = split header/body
2message_idbytes16-byte UUID
3recipientsrepeated RecipientRecordone per recipient device: ephemeral X25519 public key + the wrapped CEK (nonce 12, tag 16)
4encrypted_payloadbytesv1 only: AES-256-GCM ciphertext of the whole SignedMessage
5payload_noncebytes12 bytes
6payload_tagbytes16 bytes
7payload_size_classuint32padded size in bytes, one of the size classes
8created_atint64unix seconds
9ratchet_pub_keybytesreserved for forward secrecy (protocol v2); zero in v1
10encrypted_headerbytesv2 split: the sealed SignedHeader, small and listable (sender, subject, snippet, recipient lists, body commitments)
11header_noncebytes12 bytes
12header_tagbytes16 bytes
13header_size_classuint32padded size in bytes, one of the size classes
14encrypted_bodybytesv2 split: sealed MessageContent (body + attachments), fetchable separately from the header
15body_noncebytes12 bytes
16body_tagbytes16 bytes
17body_size_classuint32padded size in bytes, one of the size classes
18body_content_addressbytescleartext CIDv1 of the body blob, so relays can verify integrity without any key; the authoritative copy is signed inside the header
Envelope size classes
size_class valuebucket
10241 KB
40964 KB
1638416 KB
6553664 KB
262144256 KB
10485761 MB
n × 1048576above 1 MB: the payload plus its 4-byte length, rounded up to the next whole MB

Wire protocols (libp2p)#

Every DMCN conversation is a libp2p stream, an authenticated and encrypted transport whose handshake proves the peer's identity. A peer ID can't be spoofed without its key.

Three protocol IDs carry the core. Device pairing has no protocol of its own, on purpose. The pairing exchange uses the ordinary relay mailbox path, under a synthetic pairing.local domain.

libp2p protocol IDs and framing
protocol IDframingpurpose
/dmcn/relay/1.0.04-byte big-endian length prefix + protobuf; 4 MB frame cap; bodies chunked past it (to 64 MB)the workhorse: store, fetch, mailbox, resolve and onion ops
/dmcn/peers/1.0.0length-prefixed JSONcluster peer discovery: a node returns its configured peer list
/dmcn/join/1.0.0varint-delimited protobufmutual credential handshake; gates federation, deny-by-default
(extensions)—operator surfaces ride their own protocols and are not part of the core

Relay operations#

One protocol carries all relay work. Every core operation authorizes in one of three ways, and none of them is a password.

Message-authenticated ops carry their proof in the message itself. A store is valid because the sender signed the envelope, and a fetch is valid because the client answers the relay's nonce challenge with a signature from the account key. Connection-gated ops need the calling peer to have presented a valid credential at the join handshake. Public reads return signed records the reader verifies for itself. The one public write, put_record, re-verifies every record on ingest, so a record that doesn't verify can't be stored.

The operator surface (fleet administration and operator-attached entitlements) uses separate extension protocols, /dmcn/operate and /dmcn/mailbox-ext, outside the core. The arm numbers it once used here are reserved, and the table marks them.

Relay operations
#opauth classauthorization
1storemessagesender's Ed25519 signature over the envelope; the sender must resolve and pass the recipient domain's policy gates
2fetch_initmessageopens the mailbox challenge; the relay returns a fresh nonce
3fetch_proofmessageEd25519 proof over the relay's nonce with the account key; no passwords, no bearer tokens
4ackconnectioncredential-admitted federated peer
5pingpublicliveness
6mailbox_opmessagelist / body / delete and personal-KV ops, each under a fresh fetch proof
7store_initmessagechunked store for bodies past the frame cap; sender signature like store, or node role for relay→relay drain handoff
8onion_forwardconnectioncredential-admitted federated peer; peel one layer, then forward or deliver
9–18(reserved)—vacated by the operator extension protocols; do not reuse
19get_identitypublic readreturns the signed IdentityRecord; the reader verifies it against the DNS anchor
20get_darpublic readreturns the signed domain authority record; verified against the DNS fingerprint
21get_fleet_rosterpublic readfleet-root-signed node roster
22get_removalpublic readroot-signed address tombstone
23get_blocklistpublic readroot-signed credential revocation list
24put_recordself-gatingevery record is re-verified on ingest (self-certifying); a fleet may add publisher gating
25get_relay_descriptorpublic readonion descriptor, self-anchored to the relay's peer ID
26(reserved)—vacated by the operator extension protocols; do not reuse
27get_historypublic readthe address's complete rotation history; self-authenticating through the signatures on its own entries, so serving it discloses nothing a resolver could not verify

Credentials and authority#

Trust is anchored in the domain and spelled out in credentials. A credential's roles say what a key is and its grants say what it may do. No key can sign anything its grants don't cover.

Every credential chain leads back to the domain's root key, the one the DNS fingerprint anchors (Resolving an address). Delegation is monotone: an issuer can never grant more than it holds, scope can only narrow (a subdomain scope covers its children, never its parents), and chains are capped at depth 8. By convention the powerful grants stay on offline keys, and the busy online services carry only what they need to run.

The domain authority record is the domain's administrative root. It holds the issuer set, reserved local-parts, key-rotation history and policy flags. Administrative authority over an address is separate from cryptographic ownership of it. A domain authority can provision, attest and revoke bindings in its namespace, but it never holds private keys and can never read mail. How a domain owner lets a fleet operator host it is an operator extension, outside this core reference.

Credential fields
#fieldtypenotes
1versionuint32credential format version
2subjectbytesEd25519 public key: the libp2p peer ID for infrastructure and clients, the identity key for addresses
3domainstringthe domain this credential is scoped to
4addressstringrole address only: the attested local@domain
5rolesrepeated stringwhat the subject is (see the roles table)
6grantsrepeated stringwhat the subject may issue or do (see the grants table)
7attributesmap<string, string>free-form: multiaddr, ip, x25519 onion key, …
8issued_atint64unix seconds
9not_afterint64unix seconds; 0 = never expires
10scopestringauthority credentials: subdomain scope (empty = whole domain); delegation can only narrow it
11issuer_pubbytesthe domain root, or any enrolled issuer whose own grants cover this credential
12signaturebytesissuer Ed25519 over fields 1–11, 13 and 14; ctx dmcn-credential-v1\0
13relay_hintsrepeated stringrole routing only: the authoritative operator-signed mailbox relays
14effective_fromint64unix seconds not-before; the validity window is [effective_from, not_after]
15(reserved)—do not reuse
RotationEntry fields
#fieldtypenotes
1versionuint32entry format version
2addressstringthe address this transition belongs to; an entry signed for one address can never be replayed into another's history
3retired_ed25519_public_keybytesthe signing key being given up (32 bytes)
4retired_x25519_public_keybytesthe mailbox key being vacated (32 bytes)
5next_ed25519_public_keybytesthe signing key taking over (32 bytes)
6next_x25519_public_keybytesthe mailbox key taking over (32 bytes)
7rotated_atint64when the enrolling device attests this transition happened (unix seconds)
8next_revisionuint64the IdentityRecord revision this transition mints; must advance
9prev_signature_hashbytesSHA-256 of the previous entry's signature, empty at the first rotation; this is what makes truncation visible rather than silent
10authorizing_ed25519_public_keybytesthe key that produced `signature`, either the retiring key or the owner's recovery key (32 bytes)
11device_credentialCredentialthe domain's attestation of the enrolled device that authorised this rotation; its issued_at is the device's enrolment date, which a tenure rule is measured against
12device_signaturebytes64 bytes by the device; a credential alone is public and could be lifted from an earlier transition, so the device signs this one
13signaturebytesthe outgoing key's consent: 64 bytes by authorizing_ed25519_public_key, ctx dmcn-identity-rotation-v1\0
14next_signaturebytesthe incoming key's acceptance, covering the consent: 64 bytes by next_ed25519_public_key, ctx dmcn-identity-rotation-accept-v1\0
AddressHistoryRecord fields
#fieldtypenotes
1versionuint32record format version
2domainstringthe address's domain
3addressstringlocal@domain; the record is keyed on SHA-256 of this, like the removal record
4chainrepeated RotationEntrythe complete rotation history, oldest first. No container signature: every entry is already signed by the keys it names, so extending the history takes keys the extender must hold. Append-only: a stored history may only be replaced by one that strictly extends it
Credential roles
rolewhat the subject is
authoritya domain's root signing authority (its key anchors the DNS fingerprint)
sub-authoritya delegated issuer enrolled by the root
nodea relay/storage node
bridgean SMTP bridge
clientan end-user-facing client peer (mints nothing by itself)
addressan attested address↔key binding (rides in IdentityRecord.address_credential)
routingan operator routing attestation (rides in IdentityRecord.routing_credential)
deviceone enrolled device of an account, keyed by a signing key generated on that device and held nowhere else
(extensions)further roles carry operator-attached entitlements and are not part of the core
Credential grants
grantwhat the holder may do
addresssign address credentials (attest address↔key bindings)
routingsign routing credentials (attest mailbox routing)
devicesign device credentials (attest an account's enrolled devices)
grantdelegate: issue credentials that themselves carry grants
(extensions)further grants cover operator entitlements and fleet administration and are not part of the core
Domain policy flags (DAR policy_flags bits)
bitflageffect
1 << 0REQUIRE_COUNTERSIGNuncountersigned addresses are unusable on this domain; enforced reader-side and at the mailbox fetch gate
1 << 2REQUIRE_ONIONevery address on the domain must receive via onion delivery
1 << 3REPLICATE_MAILBOXsenders store to every listed relay hint instead of first-reachable failover
1 << 5ALLOW_KEY_ROTATIONaccount holders may re-key their own address with an owner-signed rotation chain; off by default
1 << 1, 1 << 4, 1 << 6 and up(extensions)reserved for extensions

Signing convention#

Every signature in DMCN follows one rule.

sig = Ed25519(priv, ctx ‖ deterministic_protobuf(message_without_signature))

The canonical form is deterministic protobuf serialization (stable map ordering) of the message with its signature field cleared. ctx is a per-type, NUL-terminated domain-separation tag, so a signature over one record type can never be replayed as another. There's one deliberate exception. The whole-message SignedMessage signature is computed over the canonical plaintext with no context tag.

Signature context tags
context tagsigns
dmcn-identity-self-v1\0IdentityRecord owner self-signature
dmcn-identity-rotation-v1\0RotationEntry: the outgoing key's consent
dmcn-identity-rotation-accept-v1\0RotationEntry: the incoming key's acceptance
dmcn-identity-rotation-device-v1\0RotationEntry: the enrolled device's attestation
dmcn-dar-self-v1\0DomainAuthorityRecord root self-signature
dmcn-credential-v1\0every Credential
dmcn-subauthority-request-v1\0a requester's self-signed sub-authority request
dmcn-address-removal-v1\0AddressRemovalRecord (root-signed tombstone)
dmcn-key-compromise-v1\0KeyCompromiseRecord
dmcn-fleet-roster-v1\0FleetRoster
dmcn-msg-header-v1\0SignedHeader (the split-format message header)
(extensions)further tags sign operator-surface records and are not part of the core

Onion routing (optional)#

An optional delivery mode that hides who is talking to whom from the relays in between. The packet format below is implemented and the design default is multi-hop, but today's deployments run single-hop, so don't count on the metadata privacy described here yet.

A route is 3 hops by default, ending at the recipient's home relay. Hops must sit in distinct /24 networks and distinct domains, so no two hops share an operator, and an optional stable guard can be the entry. Each relay publishes a RelayDescriptor with its onion key and addresses, signed by its own libp2p key. That key can be recovered from the relay's peer ID, so a descriptor verifies without trusting whoever served it.

OnionPacket { version, ephemeral_pub (32), nonce (12), tag (16), encrypted_layer }
OnionLayer  { next_hop | "DELIVER", ttl_unix, inner_packet | delivery }

each relay: derive layer key from ephemeral_pub + own X25519 key → peel → forward (or deliver)

Only the innermost delivery layer is padded to its own bucket classes (up to 36 MB, matching the message ceiling). Forwarding layers add routing overhead only. A mailbox sets require_onion (or its domain sets the REQUIRE_ONION policy) to make the relay reject direct stores with ONION_REQUIRED.