Skip to content
SwornMail docs

Implementations

Libraries and integrations

All Apache-2.0, all written by the protocol author. Two independent verifiers exist specifically so the wire format is pinned by agreement rather than by one codebase’s habits.

WhatLanguageWhere
Reference library, CLI, milterGoswornmail/swornmail-go
Independent verifierRustswornmail/swornmail · crates.io
rspamd moduleLuaswornmail/rspamd-swornmail
Draft, threat model, vectors—swornmail/spec

Go — reference implementation

$ go get github.com/swornmail/swornmail-go
$ go install github.com/swornmail/swornmail-go/cmd/sworn@latest

A receiver verifying Mode 2 tokens against live DNS uses the staged API, which enforces the protocol’s check order: every local check before any DNS, and the policy before the key.

pending, err := sworn.PrepareVerification(token, source, now) // no DNS
// fetch and parse _prefixes._sworn.<pending.Operator()>
authorized, err := pending.Authorize(policy) // no key query yet
// fetch and parse <pending.Selector()>._sworn.<pending.Operator()>
result, err := authorized.VerifySignature(key)

Key reputation on result.ObservedUnit, not result.Unit. An operator in testing mode comes back as sworn.ErrTestingMode rather than a nil error, so err == nil can never mean pass for an operator publishing t=y. sworn.Verify is the no-I/O helper for when both records are already in hand, and it requires the policy. sworn.VerifySignatureOnly exists for conformance tooling and must never be reported as a protocol pass.

sworn/discover runs Mode 1 discovery. The resolver is an interface, so both paths are testable without DNS and can be pointed at a validating resolver in production.

Dependencies are deliberately few: fxamacker/cbor, veraison/go-cose, and emersion/go-milter for the milter. Otherwise standard library.

Postfix milter

cmd/sworn-milter performs per-connection Mode 1 discovery and stamps Authentication-Results with both policy.unit and policy.observed. A testing-mode operator is reported as sworn=none policy.testing=y policy.wouldbe=pass, never as a pass. It strips inbound AR fields at the trust boundary, and it is strictly fail-open — it never rejects a message. Setup: receive with Postfix.

A milter cannot carry Mode 2. Milters see the SMTP conversation but cannot extend it: they cannot advertise an EHLO keyword or take a new SMTP verb. Mode 2 needs an MTA that speaks the extension, or a proxy in front. This is a real limitation, not an oversight.

Rust — independent verifier

Written from the draft rather than ported from the Go code, which is the entire point: an implementation derived from another implementation cannot detect that the draft is ambiguous. Writing it independently surfaced three real draft/implementation contradictions before the format was frozen.

From 0.2, verify takes the policy record as well as the key and returns an Outcome: an operator in testing mode is its own variant, so it cannot be read as a pass without noticing, and Verified carries observed_unit alongside unit. Use 0.2 or later: 0.1 predates policy authorisation. If crates.io does not yet list 0.2, build from the repository.

Lua — rspamd module

A self-contained rspamd module for Mode 1, informational by default and fail-open: every symbol scores zero until you give it weight. It reports testing-mode operators as SWORN_TESTING and emits both policy.unit and policy.observed. Its protocol logic makes no rspamd calls — the resolver is injected — so it runs under plain Lua and can be driven by the differential harness. Setup: receive with rspamd.

Conformance

The -01 wire format is frozen. Token bytes, record syntax and the conformance vectors are a public contract with shipped implementations behind them.

Test vectors

111 vectors — 62 token, 39 record and 10 policy-authorisation cases — published at spec/test-vectors. They are authored from the draft, never derived from an implementation: a vector generated by the code it is meant to check certifies nothing. An earlier round of vectors was circular in exactly that way, and rebuilding them found real defects.

Differential harnesses

Two harnesses generate adversarial corpora and run two implementations over them, comparing results:

# tokens: Go vs Rust
$ go run ./cmd/difftest --rust ../swornmail/target/debug/difftest

# records: Go vs Lua
$ go run ./cmd/recorddiff --arm ../rspamd-swornmail/test/difftest.sh

Both report zero divergences. The token corpus size is a parameter — --fuzz defaults to 3000, giving 3,065 cases — so the figure is reproducible rather than a marketing number. The record corpus is fixed at 259 cases: 236 policy records and 23 source-eligibility checks. Run the token harness larger; the divergence count should stay at zero. If it does not, that is a bug worth reporting.

Both harnesses expect the repositories to be siblings on disk. Clone them into one directory.

Writing your own

Start from the draft, not from this site and not from an existing implementation. Then run the vectors. If you disagree with a vector, that is worth an issue — it means either the draft is ambiguous or a vector is wrong, and both are defects worth fixing before anyone else hits them.