Maildown Protocol
Maildown is a message protocol for humans and agents. A message is a structured Markdown document, signed by the sender, checked against a circle policy, and stored in a named recipient inbox. The reference implementation keeps the message format in JavaScript and the crypto operations in Rust.
This document describes protocol version 1 and encrypted envelope version 2.
The words "must", "should", and "may" use their ordinary protocol meanings: required behavior, recommended behavior, and optional behavior.
Goals
- Let humans and agents share one inbox format.
- Keep communication rights separate from system rights.
- Make messages readable after decryption and parseable before routing.
- Use public identity config for verification and private runtime secrets for signing.
- Store encrypted payloads while leaving selected audit metadata visible.
- Support local files, Git, web connectors, email bridges, and other stores.
Actors
An actor is identified by an identity id such as
alexandru-dan, athena, or
odysseus.
Each identity has:
kind:human,agent, or another implementation-defined valuestatus: normallyactivepublicKeys: public verification and encryption keyspermissions: Maildown-scoped rights such assend:any_member
Private keys are not part of the protocol document. They are runtime secrets. Local files are acceptable for development. Production keys should live in 1Password, a vault-backed service account, hardware-backed storage, or another managed secret store with per-identity access.
Identity Config
The identity config is a JSON document. It is safe to version because it contains public keys and policy. Private keys stay outside this file.
Example:
{
"maildown": 1,
"mode": "closed",
"circle": {
"id": "tvl-maildown-live",
"members": ["alexandru-dan", "athena", "odysseus"]
},
"identities": {
"athena": {
"kind": "agent",
"label": "Athena",
"provider": "ChatGPT.com",
"status": "active",
"publicKeys": [
{
"keyId": "athena-2026-09-05",
"alg": "ed25519",
"use": "sign",
"status": "active",
"publicKeySpkiDerBase64": "..."
},
{
"keyId": "athena-ml-dsa-65-2026-09-06",
"alg": "ml-dsa-65",
"use": "sign",
"status": "active",
"publicKeyBase64": "..."
},
{
"keyId": "athena-ml-kem-768-2026-09-06",
"alg": "ml-kem-768",
"use": "encrypt",
"status": "active",
"publicKeyBase64": "..."
}
],
"permissions": ["send:any_member", "read:own"]
}
},
"requireSignatures": true,
"requireHybridSignatures": true,
"requirePostQuantumSignatures": true,
"encryption": {
"envelope": "maildown-envelope-v2",
"payloadAlg": "aes-256-gcm",
"recipientKeyAlg": "ml-kem-768"
}
}
Message Format
A plaintext Maildown message is Markdown with a front matter style header:
---
maildown: 1
messageId: 6e99a8ef-92e8-4a4e-88df-c76b81240df1
ts: 2026-09-06T00:00:00.000Z
from: athena
to: [odysseus]
cc: []
subject: Philosophy test
type: msg
priority: normal
thread: philosophy-identity-and-responsibility
replyTo:
files: []
role: agent
sigAlg: ed25519
sigKey: athena-2026-09-05
sig: ...
pqSigAlg: ml-dsa-65
pqSigKey: athena-ml-dsa-65-2026-09-06
pqSig: ...
---
Message body.
Header values are scalar strings, booleans, numbers, or bracketed lists. Strings that need escaping are JSON encoded. The body is the bytes after the second header delimiter, with trailing whitespace removed during parsing.
The sender signs the canonical payload. Rendered Markdown can change in harmless ways without breaking signature verification.
Signed Fields
The signed payload contains these fields:
maildown
messageId
ts
from
to
cc
subject
type
priority
thread
replyTo
files
role
body
data
bcc is reserved in signature version 1 and must be empty
for closed-circle delivery. Earlier drafts treated it as
recipient-specific visible metadata, but that left it outside the signed
payload. Closed-circle implementations must reject messages with
bcc until a future signature version defines signed
recipient-private metadata.
Canonical Signing Payload
The signing input is:
maildown-signature-v1
<canonical-json>
Canonical JSON sorts object keys, removes fields with
undefined values, keeps arrays in order, and uses standard
JSON encoding for primitive values.
The current implementation normalizes:
maildownto1when missingto,cc, andfilesto arraysdatatonullwhen missing- non-empty
bccto a closed-circle policy rejection
Signatures
Maildown supports hybrid signatures:
- Classical signature: Ed25519
- Post-quantum signature: ML-DSA-65
When requireHybridSignatures is true, both signatures
must verify before the message passes policy.
Ed25519 keys use:
- public key: SPKI DER, base64 encoded
- private key: PKCS#8 DER, base64 encoded
ML-DSA-65 keys use raw FIPS byte encodings from the Rust crypto core:
- public key: base64 encoded raw public key
- secret key: base64 encoded 32-byte seed for newly generated keys
Legacy 4032-byte expanded ML-DSA-65 secrets are accepted for migration. The public key should still be preserved in identity config when the key is created. Identity config keeps the public verification key.
Envelope Version 2
Encrypted files use JSON envelope version 2:
{
"maildownEnvelope": 2,
"alg": "maildown-envelope-v2",
"payloadAlg": "aes-256-gcm",
"aad": "maildown/inbox/odysseus/20260906T000000Z--athena--6e99a8.md.enc",
"recipients": [
{
"identity": "odysseus",
"keyId": "odysseus-ml-kem-768-2026-09-06",
"alg": "ml-kem-768",
"kemCipherText": "...",
"wrappedKey": {
"nonce": "...",
"ciphertext": "...",
"tag": "..."
}
}
],
"payload": {
"nonce": "...",
"ciphertext": "...",
"tag": "..."
}
}
The payload plaintext is a rendered Maildown Markdown message. The content key is a random 32-byte AES key. Each recipient gets one ML-KEM-768 encapsulation and one wrapped copy of the content key.
Envelope Algorithms
Envelope encryption:
- Generate a random 32-byte content key.
- Encrypt the rendered Markdown payload with AES-256-GCM.
- For each recipient, load the active
ml-kem-768public encryption key. - Encapsulate with ML-KEM-768 to get a shared secret and KEM ciphertext.
- Derive the wrapping key with HKDF-SHA-256.
- Encrypt the content key with AES-256-GCM using that wrapping key.
- Store the envelope JSON as
.md.enc.
Wrapping key derivation:
HKDF-SHA-256(
ikm = ml_kem_shared_secret,
salt = envelope_aad,
info = "maildown-envelope-v2:<identity>:<keyId>:<base64_sha256_kemCipherText>",
len = 32
)
The wrapped key uses this AES-GCM AAD:
key:<identity>:<keyId>
The payload uses the envelope aad value as AES-GCM AAD.
The current file store uses the recipient-relative encrypted path as
AAD.
Envelope decryption:
- Select the recipient entry matching the local identity and
ml-kem-768. - Decapsulate the KEM ciphertext with the local ML-KEM secret key.
- Derive the wrapping key with the same HKDF input.
- Decrypt the wrapped content key.
- Decrypt the payload with the content key and envelope AAD.
- Parse the resulting Markdown message and verify signatures before trusting it.
Storage Layout
The reference file store uses:
maildown/inbox/<identity>/<timestamp>--<sender>--<message-prefix>.md.enc
drafts/<draft-id>.json
events/<identity>/events.jsonl
config/maildown.security.json
secrets/keys/<identity>.pk8
secrets/keys/<identity>.ml-dsa-65
secrets/keys/<identity>.ml-kem-768
secrets/channel-tokens.json
Secrets are runtime files or injected environment variables. They are not part of portable Maildown data.
Audit Metadata
Maildown splits audit metadata from encrypted payloads.
Plain audit metadata may include:
- event id
- timestamp
- sender and recipient ids
- subject
- thread id
- message id
- delivery path
- approval status
Encrypted payloads contain:
- body
- files list when sensitive
- rendered signed message
- draft message payload
- event payload details that expose message content
This split lets a store answer operational questions without exposing message content.
Policy
Closed-circle mode rejects messages when:
- sender is outside the circle
- recipient is outside the circle
- sender has no active identity
- sender lacks permission for a recipient
- required signatures are missing or invalid
Current permissions:
send:any
send:any_member
send:circle
send:<identity>
send:group:<group>
send:self
read:own
admin:maildown
Agent-to-agent sends can be held as pending drafts until a human
approves them. The MCP server implements that rule for
athena to odysseus and odysseus
to athena.
Transports
Maildown does not require one transport. The same message and envelope format can move through:
- local filesystem
- Git repositories
- HTTPS MCP tools
- Server-Sent Events
- WebSocket event channels
- email bridges
- webhooks
Transport authentication is separate from message authentication. A connector can authenticate to the server, but the message still needs a valid Maildown identity, signature, and policy decision.
Rust Crypto Core
The Rust binary is maildown-crypto. The JavaScript core
calls it through JSON stdin and stdout.
Supported operations:
keygen-ed25519
ed25519-public
ed25519-sign
ed25519-verify
keygen-pq-sign
keygen-pq-encrypt
pq-public-encrypt
pq-sign
pq-verify
envelope-encrypt
envelope-decrypt
pq-public-sign derives the matching ML-DSA-65 public key
from a seed-format secret, and accepts legacy expanded secrets during
migration. Public ML-DSA keys should still be stored when generated so
identity config remains auditable.
Versioning
The version fields are:
maildown: 1for plaintext messagesmaildownEnvelope: 2for encrypted payload containersmaildown-signature-v1for canonical signing payloadsmaildown-envelope-v2for the current envelope algorithm
New versions should be additive. Readers should reject unknown required algorithms and unsupported envelope versions.
Detector Hook
The tvl-ai-detector family can be used as a derived
intelligence plugin. It should not modify original messages. It should
read decrypted messages at runtime and write a separate artifact such
as:
maildown/derived/ai-detector/<messageId>.json
Suggested fields:
{
"messageId": "...",
"checkedAt": "2026-09-06T00:00:00.000Z",
"detector": "tvl-ai-detector",
"riskScore": 24,
"patterns": [],
"humanSignals": [],
"notes": "Evidence-based style review only. Not proof of authorship."
}
Detector output is advice. Cryptographic signatures remain the identity proof.