tpt-email
GoOpen-source reference gateway for Trust-First Email Protocol (TFEP) — cryptographic sender identity via Ed25519 + DID:web, spam/phishing prevention, verified webhooks, B2B document exchange, and non-repudiable legal notices. SMTP-compatible. Written in Go.
Languages
TFEP Gateway
Trust-First Email Protocol — a backward-compatible email successor that cryptographically solves spam, phishing, and identity spoofing without breaking a single existing email client.
Three Problems. One Protocol.
TFEP's core primitive — cryptographic sender identity via Ed25519 + DID:web — solves the same trust gap in three high-value markets:
| Use Case | What you get | Guide | |---|---|---| | Verified Webhooks | Replace HMAC shared secrets with unforgeable DID-based identity. Any Go service can sign/verify in 3 lines. | WEBHOOKS.md | | B2B Document Exchange | Send invoices, contracts, and purchase orders with cryptographic sender proof and permit-based vendor authorization. No EDI VAN required. | B2B-EXCHANGE.md | | Legal / Regulatory Notices | Non-repudiable notice delivery with mandatory delivery receipts and timestamp proofs. | LEGAL-NOTICES.md |
Why TFEP?
Email's trust problems are structural. SPF, DKIM, and DMARC are bolt-on fixes to a protocol that was never designed for verification. Anyone can forge "PayPal Security" <attacker@evil.com> and have it land in your inbox.
TFEP redesigns the trust layer from scratch while staying fully compatible with SMTP:
| Problem | How TFEP solves it |
|---|---|
| Spam | Senders declare message intent (personal, transactional, marketing). Marketing without a cryptographic opt-in permit is rejected at the protocol level — not filtered after delivery. |
| Phishing | Every TFEP message carries an Ed25519 signature verifiable against the sender's published DID document. Forging did:web:paypal.com requires controlling PayPal's DNS and private key. |
| Display-name spoofing | The verified DID domain is surfaced regardless of the From display name. Brand-name mismatches are flagged at the gateway. |
| Unsolicited transactional mail | A fake "your account is suspended" email from an unknown sender has no relationship token and is downgraded to the held queue. |
TFEP does not require everyone to switch at once. It works as a dual-stack overlay: TFEP gateways speak both TFEP and SMTP, translating between them transparently.
Features
- Cryptographic sender identity — Ed25519 signatures anchored to domain DNS via DID:web, no blockchain
- End-to-end encryption — X25519 ECDH + NaCl secretbox, automatic when both parties have TFEP capability, graceful plaintext fallback to SMTP
- Permission tokens — Recipients issue signed JWT permits; senders without one go to held queue
- Message type enforcement — Marketing without opt-in permit rejected with SMTP 554; not filtered, rejected
- Anti-phishing pipeline — Signature check, display-name mismatch detection, relationship token enforcement, replay prevention
- Proof-of-work — Configurable Hashcash difficulty makes bulk sending expensive; exempt for permitted senders
- Domain name registry — Lifetime registrations with cryptographic ownership transfer; no renewals
- DNS bridge — Serves registered names under a custom TLD (
.tpt) so addresses likealice@someone.tptwork - REST API + web UI — Inbox, held queue, permit management, contact book — no client install required
- Prometheus metrics + structured logging — Production-ready observability
- Single binary —
go buildproduces one statically-linked executable; no runtime dependencies
Quick Start (5 minutes)
Install
go install github.com/tfep/tfep-gateway/cmd/tfep-gateway@latest
Or build from source:
git clone https://github.com/tfep/tfep-gateway.git
cd tfep-gateway
go build -o tfep-gateway ./cmd/tfep-gateway
Generate your identity
./tfep-gateway keygen --domain example.com --passphrase "your-secret"
This outputs:
- A DID document — save it at
https://example.com/.well-known/did.json - A DNS TXT record — add it to your DNS as
_tfep.example.com - Two encrypted key files:
ed25519.pem,x25519.pem
Configure
Edit config.yaml:
domain: "example.com"
identity:
did: "did:web:example.com"
priv_key_file: "ed25519.pem"
x25519_file: "x25519.pem"
api:
key: "your-strong-api-key"
tls:
cert_file: "/etc/letsencrypt/live/example.com/fullchain.pem"
key_file: "/etc/letsencrypt/live/example.com/privkey.pem"
Run
TFEP_KEY_PASSPHRASE="your-secret" ./tfep-gateway serve
Open http://localhost:8080 for the web inbox.
Architecture
┌─────────────────────────────────────────┐
│ TFEP Gateway │
│ │
SMTP sender ──────► :25 MTA server │
│ │ │
TFEP sender ──────► :587 submission │
│ │ │
│ ▼ │
│ Trust Pipeline │
│ ① DNS TXT + DID:web lookup │
│ ② Ed25519 signature verify │
│ ③ Display-name spoof check │
│ ④ Message type rule enforcement │
│ ⑤ Permit / relationship token check │
│ ⑥ Proof-of-work check │
│ ⑦ Replay / timestamp freshness check │
│ ⑧ Route to trust-tiered queue │
│ │
│ Tiers: verified / identified / │
│ unknown / blocked │
│ │
│ SQLite store ◄──── REST API :8080 │
│ │ │
│ ▼ │
│ Web UI │
└─────────────────────────────────────────┘
Trust Tiers
| Tier | Condition | Inbox routing | |------|-----------|---------------| | verified | Valid TFEP DNS record + DID document + Ed25519 signature | Inbox | | identified | DKIM/SPF/DMARC but no TFEP | Normal | | unknown | No verifiable authentication | Held queue | | blocked | Local blocklist match | Rejected |
Message Types
| Type | Authorization required |
|------|----------------------|
| personal | Permit or verified mutual contact |
| transactional | Relationship token issued at signup |
| marketing | Explicit opt-in permit from recipient |
| automated | Same as transactional + rate limited |
CLI Reference
tfep-gateway [--config config.yaml] <command>
serve
Start all servers (MTA, submission, API).
tfep-gateway serve
TFEP_KEY_PASSPHRASE=secret tfep-gateway serve --config /etc/tfep/config.yaml
keygen
Generate a new Ed25519/X25519 keypair. Outputs DID document and DNS TXT record.
tfep-gateway keygen --domain example.com
tfep-gateway keygen --domain example.com --passphrase "secret"
rotate-key
Generate a replacement keypair for key rotation. Does not overwrite existing keys.
tfep-gateway rotate-key --domain example.com --passphrase "new-secret"
# Outputs: ed25519.new.pem, x25519.new.pem
# Update /.well-known/did.json, then replace the old key files and restart.
send
Send a signed (optionally encrypted) test message.
tfep-gateway send \
--to alice@example.org \
--type personal \
--subject "Hello from TFEP" \
--body "This message is cryptographically signed."
queue list
List stored messages with optional filters.
tfep-gateway queue list # all messages
tfep-gateway queue list --tier unknown # held queue
tfep-gateway queue list --type marketing # marketing messages
tfep-gateway queue list --thread <uuid> # messages in a thread
queue release
Promote a held message to verified and auto-issue a send permit to the sender.
tfep-gateway queue release <message-id>
permits list / revoke
Manage active send permits.
tfep-gateway permits list
tfep-gateway permits revoke <permit-id>
store vacuum
Delete messages that have exceeded their retention policy.
tfep-gateway store vacuum
REST API
The API listens on port 8080 (configurable). All endpoints except /healthz, /metrics, and the well-known endpoints require Authorization: Bearer <api_key>.
Messages
| Method | Path | Description |
|--------|------|-------------|
| GET | /api/v1/messages | List messages. Query: ?tier=, ?type=, ?thread= |
| GET | /api/v1/messages/{id} | Get a message |
| GET | /api/v1/messages/{id}/raw | Get raw MIME (message/rfc822) |
| POST | /api/v1/messages/{id}/release | Promote to verified + issue permit |
| DELETE | /api/v1/messages/{id} | Delete |
Permits
| Method | Path | Description |
|--------|------|-------------|
| GET | /api/v1/permits | List active permits |
| POST | /api/v1/permits | Issue permit {"grantee_did":"…","expiry_hours":8760} |
| DELETE | /api/v1/permits/{id} | Revoke |
Contacts
| Method | Path | Description |
|--------|------|-------------|
| GET | /api/v1/contacts | List contacts |
| POST | /api/v1/contacts | Add {"did":"…","display_name":"…"} |
| DELETE | /api/v1/contacts/{id} | Remove |
Registry
| Method | Path | Description |
|--------|------|-------------|
| GET | /api/v1/registry/{name} | Resolve a registered name |
| GET | /api/v1/registry/search?q= | Search names |
| POST | /api/v1/registry/register | Register a name (auth required) |
| POST | /api/v1/registry/transfer | Transfer ownership (auth required) |
System
| Method | Path | Description |
|--------|------|-------------|
| GET | /healthz | Health check + timestamp |
| GET | /metrics | Prometheus metrics |
| GET | /api/v1/stats | Queue depth by tier |
Well-known Protocol Endpoints
These are called by remote TFEP senders — no auth required.
| Method | Path | Description |
|--------|------|-------------|
| POST | /.well-known/tfep/permit-request | Sender requests a send permit |
| POST | /.well-known/tfep/unsubscribe | Withdraw a marketing permit |
Configuration Reference
Full annotated config with all defaults:
# Your email domain. Must match the did:web domain.
domain: "example.com"
smtp:
listen_addr: ":25" # MTA inbound port (requires root or CAP_NET_BIND_SERVICE)
submission_addr: ":587" # Submission port (STARTTLS)
max_msg_bytes: 26214400 # 25 MB
tls:
cert_file: "" # Path to TLS certificate (PEM)
key_file: "" # Path to TLS private key (PEM)
api:
listen_addr: ":8080" # REST API + web UI port
key: "" # Bearer token (leave empty to disable auth in dev)
identity:
did: "did:web:example.com"
priv_key_file: "ed25519.pem"
x25519_file: "x25519.pem"
dkim_selector: "" # DKIM selector for outbound signing (optional)
dkim_key_file: "" # DKIM private key file (optional)
require_dnssec: false # Hard-reject TFEP lookups without DNSSEC validation
trust:
pow_difficulty: 0 # Hashcash bits required (0 = disabled, 16 = ~65k ops)
permit_ttl: "8760h" # Default permit validity (1 year)
reputation_ttl: "720h" # Reputation token validity (30 days)
max_message_age: "5m" # Reject messages older than this (replay prevention)
clock_skew: "1m" # Tolerate timestamps up to this far in the future
send_receipts: false # Send delivery acknowledgments to TFEP senders
auto_approve_permits: false # Auto-grant permits from /.well-known/tfep/permit-request
rate_limit:
connections_per_minute: 20
messages_per_hour: 100
federated_reputation:
enabled: false
resolver: "rep.tfep.net" # Community reputation DNS zone
store:
path: "tfep.db" # SQLite database file
retention: # 0 = keep forever
verified: "2160h" # 90 days
identified: "720h" # 30 days
unknown: "168h" # 7 days
blocked: "24h" # 1 day
registry:
enabled: false
tld: "tpt" # Custom TLD for registered names (e.g. alice@someone.tpt)
dormant_days: 90 # Days after registration before marking dormant
dns_bridge:
enabled: false
listen_addr: ":5353" # DNS server port (use :53 for standard DNS)
upstream: "1.1.1.1:53" # Upstream resolver for non-registry queries
gateway_ip: "" # IP address returned for A records of registered names
log:
level: "info" # debug | info | warn | error
format: "text" # text | json
Environment Variables
All config values can be overridden with TFEP_ prefix environment variables:
TFEP_DOMAIN=example.com
TFEP_API_KEY=your-secret-key
TFEP_KEY_PASSPHRASE=your-key-passphrase
TFEP_LOG_LEVEL=debug
Deployment
Binary (recommended for VPS)
# Build
go build -ldflags="-s -w" -o tfep-gateway ./cmd/tfep-gateway
# Create system user
useradd -r -s /bin/false tfep
# Install
install -m 755 tfep-gateway /opt/tfep-gateway/
install -m 640 config.yaml /etc/tfep-gateway/
# Systemd
cp tfep-gateway.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now tfep-gateway
The systemd unit runs as a non-root user with NoNewPrivileges, PrivateTmp, and a capability bounding set limited to CAP_NET_BIND_SERVICE.
Docker
docker compose up -d
The docker-compose.yml mounts config.yaml and key files as read-only volumes and persists the SQLite database to a named volume.
# Minimal docker-compose.yml override for production
services:
tfep-gateway:
environment:
- TFEP_DOMAIN=example.com
- TFEP_API_KEY=your-strong-key
- TFEP_KEY_PASSPHRASE=your-key-passphrase
ports:
- "25:25"
- "587:587"
- "8080:8080"
DNS Setup
After generating your keypair:
-
TXT record — Add to DNS:
_tfep.example.com TXT "v=TFEP1; did=did:web:example.com; policy=bridge; pow=0; caps=v1,enc,pow" -
DID document — Publish at
https://example.com/.well-known/did.json(the keygen command outputs the exact JSON) -
MX record — Ensure your domain has an MX record pointing to this server's IP
Domain Name Registry
The optional registry allows permanent registration of short names under a custom TLD (default .tpt). Unlike traditional domain registrars, registrations are lifetime and transfers require a cryptographic signature from the current owner.
Enable in config:
registry:
enabled: true
tld: "tpt"
dns_bridge:
enabled: true
listen_addr: ":53"
gateway_ip: "1.2.3.4" # your server's public IP
Register a name:
curl -X POST http://localhost:8080/api/v1/registry/register \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{"name":"alice","did":"did:web:example.com","owner_pubkey":"<hex-ed25519-pubkey>"}'
With the DNS bridge pointing at your gateway, alice.tpt will resolve to your gateway IP, allowing TFEP-native addresses like tfep@alice.tpt.
Protocol Specification
The full wire format, security properties, and migration strategy are documented in PROTOCOL.md.
Key sections:
- §2 Identity model (DID:web, DNS TXT records, key rotation)
- §3 Message format (MIME structure, envelope schema)
- §4 Message types and authorization rules
- §5 Trust tiers
- §6 Permission tokens (send permits)
- §7 End-to-end encryption
- §8 Proof-of-work
- §9 Anti-phishing enforcement
- §10 Reputation tokens (SMTP bootstrap)
- §11 Federated reputation
- §15 Migration path
Roadmap
The v0.1.0 release is a complete reference implementation. Planned community work:
- IMAP4 server — so existing email clients (Thunderbird, Apple Mail) can access TFEP inboxes
- Native client — web and mobile clients that show trust badges, thread views, and verified sender information
- ACME/Let's Encrypt — automatic TLS certificate provisioning
- Community reputation zone —
rep.tfep.netDNS infrastructure for federated spam scoring - Formal security audit — third-party review of the cryptographic layer
See CONTRIBUTING.md for areas that need help.
Contributing
Contributions are welcome. Read CONTRIBUTING.md for the development setup, code standards, and process.
To report a security vulnerability, see SECURITY.md.
License
MIT — see LICENSE.
Built with: go-smtp, go-message, go-msgauth, miekg/dns, modernc/sqlite, golang-jwt/jwt, prometheus/client_golang, spf13/cobra, spf13/viper.