1. Status of this document
This is a reference snapshot of the running implementation — versioned with the code, generated in part from it, and corrected by it: where this page and the implementation disagree, the implementation wins.
Scope: core protocol only. This page documents what an implementation needs to interoperate — resolve, verify, send, receive. The protocol also defines operator surfaces (fleet provisioning and administration, operator-attached quotas and access windows); those are optional segments a fleet may run, never requirements for interoperation, and are deliberately left out here until they are documented as proper extensions. Where they touch a shared structure below, rows are marked (extension) so the schema stays fully accounted for.
The wire-reference tables below (identity record, credential, envelope, relay operations, tiers) are built from the protocol's protobuf schema — the same definitions the implementation compiles against — so field names and numbers on this page are correct by construction. The core protocol spec and an Apache-2.0 reference implementation are published in full at github.com/dmcnp/open-dmcn — anyone can read the wire format, audit it, or run their own server. It is an early release that tracks the implementation rather than a frozen standard: formal versioning and a conformance suite are on the roadmap.
Conventions: messages use the protobuf3 wire format; byte lengths appear in parentheses; where the protocol frames data by hand, integers are big-endian; signature context tags are NUL-terminated strings, written here as \0. Reserved field numbers must not be reused.
2. Identity & addressing
An address — alice@yourcompany.com — is a keypair: an Ed25519 signing key and an X25519 key-agreement key, bound in a self-certifying IdentityRecord that proves itself to any reader. No certificate authority issues it.
The record has two ownership zones. The identity core — address, keys, validity, tier, revision — is covered by the owner's self-signature: only the key holder can change it. The operational side — mailbox routing — rides as an operator-signed credential deliberately excluded from the self-signature, so a domain operator can re-point or rebalance routing without ever holding the owner's key (the credential verifies against the domain authority instead — see §7).
The owner-signed revision is monotonic: fleets and readers reject any record whose revision is lower than one they have already verified, so a replayed old record can never roll an identity back.
Wire reference: IdentityRecord fields
| # | field | type | notes |
|---|---|---|---|
| 1 | version | uint32 | record format version |
| 2 | address | string | local@domain |
| 3 | ed25519_public_key | bytes | signing key (32 bytes) |
| 4 | x25519_public_key | bytes | key-agreement key (32 bytes) |
| 5 | created_at | int64 | unix seconds |
| 6 | expires_at | int64 | unix seconds; 0 = no expiry |
| 7 | relay_hints | repeated string | reader-facing mirror only — NOT covered by the self-signature; the authoritative copy lives inside routing_credential (25) |
| 8 | verification_tier | VerificationTier | trust tier — see the tier table; enforcement is reader-side |
| 9 | attestations | repeated AttestationRecord | web-of-trust attestations (in-person, fingerprint, network, organizational) |
| 10 | self_signature | bytes | owner Ed25519 (64 bytes) over fields 1–6, 8, 23, 26; ctx dmcn-identity-self-v1\0 |
| 11–22 | (reserved) | — | do not reuse |
| 23 | require_onion | bool | mailbox requires onion delivery — relays reject direct stores; covered by the self-signature |
| 24 | address_credential | Credential | the domain's attestation of the address↔key binding (role address); issued after self-signing, excluded from the self-signature |
| 25 | routing_credential | Credential | operator-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 |
| 26 | revision | uint64 | monotonic and owner-signed — anti-rollback: a lower revision can never overwrite a higher one |
| 27 | (reserved) | — | do not reuse |
| 28 | operator_credentials | repeated Credential | generic 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 |
Wire reference: verification tiers
| value | name | meaning |
|---|---|---|
| 0 | VERIFICATION_TIER_UNVERIFIED | valid but untrusted — addresses register here and still function; trust is upgraded, never gated at registration |
| 1 | (reserved) | do not reuse |
| 2 | VERIFICATION_TIER_DOMAIN_DNS | raised by a domain attestation (address credential) chained to the DNS-anchored domain authority |
| 3 | VERIFICATION_TIER_DANE | reserved highest tier: DNSSEC/DANE-anchored |
3. Resolution: the _dmcn record
Addresses resolve against the domain's own infrastructure, discovered through DNS — the same delegation model MX records use for mail routing. There is no global directory, so no foreign majority can censor or dominate resolution.
_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 — discovery only; spoofing it is DoS, never forgery seed bootstrap multiaddrs; each ends /p2p/<peerID>, so the transport handshake authenticates the endpoint
A resolver dials a seed and fetches the domain authority record and the identity record, then verifies — in order:
- the domain authority record's root self-signature is valid;
- the authority's fingerprint equals the DNS fp= anchor (fail-closed);
- if the authority declares a fleet domain, it matches the DNS fleet= deferral (opt-in pinning);
- the identity record's owner self-signature is valid;
- the address credential chains to the domain authority (§7);
- neither key is blocked and the address is not tombstoned.
Because every record is self-certifying and anchored in the mailbox domain's DNS, a wrong or hostile server can only fail to answer — it can never fabricate or tamper undetected. Failure semantics are deliberate: no _dmcn record means the address does not exist; a transient DNS failure fails closed rather than downgrading trust.
4. Message model & encryption
A message is composed in plaintext, signed by the sender's key, then sealed once with a fresh per-message key that is wrapped separately for each recipient — three layers: PlaintextMessage → SignedMessage → EncryptedEnvelope.
Envelope version 2 splits the sealed content in two: a small, listable header (sender, subject, snippet, recipient lists, and signed commitments to the body) and the body (content and attachments), fetchable independently — 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 rides 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 — that 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 detects a message re-targeted at someone it was not addressed to: a legitimate recipient holds the message key, so it can re-seal the same signed header to a third party — but it cannot 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 reveals as little as possible; the padded layout is [4-byte BE length][payload][zero padding]. The body blob also carries a cleartext content address 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
Honest edge: v1 provides 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.
Wire reference: EncryptedEnvelope fields
| # | field | type | notes |
|---|---|---|---|
| 1 | version | uint32 | 1 = single-blob payload; 2 = split header/body |
| 2 | message_id | bytes | 16-byte UUID |
| 3 | recipients | repeated RecipientRecord | one per recipient device: ephemeral X25519 public key + the wrapped CEK (nonce 12, tag 16) |
| 4 | encrypted_payload | bytes | v1 only: AES-256-GCM ciphertext of the whole SignedMessage |
| 5 | payload_nonce | bytes | 12 bytes |
| 6 | payload_tag | bytes | 16 bytes |
| 7 | payload_size_class | uint32 | padded size bucket — see size classes |
| 8 | created_at | int64 | unix seconds |
| 9 | ratchet_pub_key | bytes | reserved for forward secrecy (protocol v2); zero in v1 |
| 10 | encrypted_header | bytes | v2 split: sealed SignedHeader — small and listable (sender, subject, snippet, recipient lists, body commitments) |
| 11 | header_nonce | bytes | 12 bytes |
| 12 | header_tag | bytes | 16 bytes |
| 13 | header_size_class | uint32 | padded size bucket |
| 14 | encrypted_body | bytes | v2 split: sealed MessageContent (body + attachments), fetchable separately from the header |
| 15 | body_nonce | bytes | 12 bytes |
| 16 | body_tag | bytes | 16 bytes |
| 17 | body_size_class | uint32 | padded size bucket |
| 18 | body_content_address | bytes | cleartext CIDv1 of the body blob — lets relays verify integrity without any key; the authoritative copy is signed inside the header |
Wire reference: envelope size classes
| class | bucket |
|---|---|
| 1 | 1 KB |
| 2 | 4 KB |
| 3 | 16 KB |
| 4 | 64 KB |
| 5 | 256 KB |
| 6 | 1 MB |
| 7+ | above 1 MB: round up to the next whole MB |
5. Wire protocols (libp2p)
Every DMCN conversation is a libp2p stream: authenticated, encrypted transport where the peer's identity is cryptographically proven in the handshake — a peer ID cannot be spoofed without its key.
Three protocol IDs carry the core. Device pairing deliberately has no protocol of its own — the pairing exchange rides the ordinary relay mailbox path (under a synthetic pairing.local domain), with the human-verified SAS code committed under the dmcn-pair-sas-v1\0 context.
Wire reference: libp2p protocol IDs & framing
| protocol ID | framing | purpose |
|---|---|---|
| /dmcn/relay/1.0.0 | 4-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 (§6) |
| /dmcn/peers/1.0.0 | length-prefixed JSON | cluster peer discovery — a node returns its configured peer list |
| /dmcn/join/1.0.0 | varint-delimited protobuf | mutual credential handshake; gates federation, deny-by-default |
| (extensions) | — | the operator and client-extension surfaces ride separate product protocols (/dmcn/operate, /dmcn/mailbox-ext, provisioning, permits) — not part of the core protocol; to be documented separately |
6. Relay operations & authorization
One protocol carries all relay work, and every core operation authorizes in one of three ways — none of which is a password.
Message-authenticated ops carry their proof in the message itself: a store is valid because the sender signed the envelope; a fetch is valid because the client answers the relay's nonce challenge with a signature from the account key. Connection-gated ops require the calling peer to have presented a valid credential at the join handshake. Public reads return signed records the reader verifies independently; the one public write, put_record, self-gates by re-verifying every record on ingest — an unverifiable record simply cannot be stored. The operator surface — fleet administration and operator-attached entitlements — rides two separate product-side protocols (/dmcn/operate, /dmcn/mailbox-ext) outside the core; the arm numbers it once occupied here are reserved, and the table marks them so.
Wire reference: relay operations (core)
| # | op | auth class | authorization |
|---|---|---|---|
| 1 | store | message | sender's Ed25519 signature over the envelope; the sender must resolve and pass the recipient domain's policy gates |
| 2 | fetch_init | message | opens the mailbox challenge — the relay returns a fresh nonce |
| 3 | fetch_proof | message | Ed25519 proof over the relay's nonce with the account key; no passwords, no bearer tokens |
| 4 | ack | connection | credential-admitted federated peer |
| 5 | ping | public | liveness |
| 6 | mailbox_op | message | list / body / delete, mail-filter and personal-KV ops — each under a fresh fetch proof |
| 7 | store_init | message | chunked store for bodies past the frame cap; sender signature like store, or node role for relay→relay drain handoff |
| 8 | onion_forward | connection | credential-admitted federated peer — peel one layer, forward or deliver |
| 9–18 | (reserved) | — | vacated by the operator extension protocols; do not reuse |
| 19 | get_identity | public read | returns the signed IdentityRecord; the reader verifies it against the DNS anchor |
| 20 | get_dar | public read | returns the signed domain authority record; verified against the DNS fingerprint |
| 21 | get_fleet_roster | public read | fleet-root-signed node roster |
| 22 | get_removal | public read | root-signed address tombstone |
| 23 | get_blocklist | public read | root-signed credential revocation list |
| 24 | put_record | self-gating | every record is re-verified on ingest (self-certifying); a fleet may add publisher gating |
| 25 | get_relay_descriptor | public read | onion descriptor, self-anchored to the relay's peer ID |
| 26 | (reserved) | — | vacated by the operator extension protocols; do not reuse |
7. Credentials, authority & permits
Trust is domain-anchored and explicit: a credential's roles say what a key is, its grants say what it may do — and no key can sign anything its grants don't cover.
Every credential chain resolves to the domain's root key — the one anchored by the DNS fingerprint (§3). 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. Powerful grants stay on offline keys by convention; the high-traffic online services carry only what they operationally need.
The domain authority record is the domain's administrative root: its issuer set, reserved local-parts, key-rotation history, and policy flags. Administrative authority over an address is distinct from cryptographic ownership of it — a domain authority can provision, attest and revoke bindings on its namespace, but it never holds private keys and can never read mail. Hosting-consent mechanics between domain owners and fleet operators exist as an operator extension, outside this core reference.
Wire reference: Credential fields
| # | field | type | notes |
|---|---|---|---|
| 1 | version | uint32 | credential format version |
| 2 | subject | bytes | Ed25519 public key — the libp2p peer ID for infrastructure and clients, the identity key for addresses |
| 3 | domain | string | the domain this credential is scoped to |
| 4 | address | string | role address only: the attested local@domain |
| 5 | roles | repeated string | what the subject IS — see the roles table |
| 6 | grants | repeated string | what the subject may ISSUE or DO — see the grants table |
| 7 | attributes | map<string, string> | free-form: multiaddr, ip, x25519 onion key, … |
| 8 | issued_at | int64 | unix seconds |
| 9 | not_after | int64 | unix seconds; 0 = never expires |
| 10 | scope | string | authority credentials: subdomain scope (empty = whole domain); delegation can only narrow it |
| 11 | issuer_pub | bytes | the domain root, or any enrolled issuer whose own grants cover this credential |
| 12 | signature | bytes | issuer Ed25519 over fields 1–11, 13 and 14; ctx dmcn-credential-v1\0 |
| 13 | relay_hints | repeated string | role routing only: the authoritative operator-signed mailbox relays |
| 14 | effective_from | int64 | unix seconds not-before; the validity window is [effective_from, not_after] |
Wire reference: credential roles (core)
| role | what the subject is |
|---|---|
| authority | a domain's root signing authority (its key anchors the DNS fingerprint) |
| sub-authority | a delegated issuer enrolled by the root |
| node | a relay/storage node |
| bridge | an SMTP bridge |
| client | an end-user-facing client peer (mints nothing by itself) |
| address | an attested address↔key binding (rides in IdentityRecord.address_credential) |
| routing | an operator routing attestation (rides in IdentityRecord.routing_credential) |
| (extensions) | further roles carry operator-attached entitlements — not part of the core protocol; to be documented separately |
Wire reference: credential grants (core)
| grant | what the holder may do |
|---|---|
| address | sign address credentials (attest address↔key bindings) |
| routing | sign routing credentials (attest mailbox routing) |
| grant | delegate: issue credentials that themselves carry grants |
| (extensions) | further grants cover operator entitlements and fleet administration — not part of the core protocol; to be documented separately |
Wire reference: domain policy flags (DAR policy_flags bits, core)
| bit | flag | effect |
|---|---|---|
| 1 << 0 | REQUIRE_COUNTERSIGN | uncountersigned addresses are unusable on this domain — enforced reader-side and at the mailbox fetch gate |
| 1 << 2 | REQUIRE_ONION | every address on the domain must receive via onion delivery |
| 1 << 3 | REPLICATE_MAILBOX | senders store to every listed relay hint instead of first-reachable failover |
| 1 << 1, 1 << 4 | (extensions) | operator surface — not part of the core protocol; to be documented separately as an optional extension |
8. 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. One deliberate exception: the whole-message SignedMessage signature is computed over the canonical plaintext with no context tag.
Wire reference: signature context tags (core)
| context tag | signs |
|---|---|
| dmcn-identity-self-v1\0 | IdentityRecord owner self-signature |
| dmcn-dar-self-v1\0 | DomainAuthorityRecord root self-signature |
| dmcn-credential-v1\0 | every Credential |
| dmcn-address-removal-v1\0 | AddressRemovalRecord (root-signed tombstone) |
| dmcn-key-compromise-v1\0 | KeyCompromiseRecord |
| dmcn-fleet-roster-v1\0 | FleetRoster |
| dmcn-msg-header-v1\0 | SignedHeader (the split-format message header) |
| dmcn-pair-sas-v1\0 | the device-pairing SAS commitment |
| (extensions) | further tags sign operator-surface records — not part of the core protocol; to be documented separately |
9. 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; today's deployments run single-hop, and multi-hop is the design default — stated plainly so nobody buys metadata privacy this page hasn't earned 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 (no two hops share an operator), with an optional stable guard as the entry. Each relay publishes a RelayDescriptor — its onion key and addresses — signed by its own libp2p key, which is recoverable from its peer ID, so descriptors verify without trusting whoever served them.
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.