- Go 98.3%
- Shell 1.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Declare the project license explicitly: add the canonical Apache-2.0 LICENSE text, a NOTICE file attributing the Apache-2.0/MIT dependencies, an SPDX-License-Identifier header on every .go file, and a License section in the README. |
||
| .claude/memory | ||
| assets | ||
| cmd/mailseal | ||
| doc | ||
| internal | ||
| list-of-free-tsa@b2af59decf | ||
| man | ||
| testdata | ||
| TSA | ||
| .gitignore | ||
| .gitmodules | ||
| .golangci.yml | ||
| AGENTS.md | ||
| api_test.go | ||
| authority.go | ||
| doc.go | ||
| event.go | ||
| fuzz_verify_test.go | ||
| go.mod | ||
| go.sum | ||
| header.go | ||
| LICENSE | ||
| normalize.go | ||
| NOTICE | ||
| public_test.go | ||
| query.go | ||
| README.md | ||
| result.go | ||
| sign.go | ||
| sign_e2e_test.go | ||
| sign_test_helpers_test.go | ||
| sign_verify_regression_test.go | ||
| token.go | ||
| verify.go | ||
mailseal
Turn ordinary email into durable, independently verifiable evidence — with one command and no service to operate.
mailseal reads an RFC 5322
message, creates a
reproducible canonical representation, obtains one or more trusted
RFC 3161 timestamps, validates
them, and embeds the proof in an x-tsa-signature header. The result is a
self-contained email that can be verified later without a mailseal database or
daemon.
One binary. Standard input and output. Open formats. Explicit trust. Use it as a Unix-style command or import the same security-critical implementation as a Go package.
Why mailseal?
| Principle | What it means here |
|---|---|
| KISSS — Keep It Simple, Secure & Small | Pipe a message in and get a sealed message out. No database, sidecar, agent, or proprietary service is required. |
| Portable | A standalone Go executable with embedded manuals; the same core is available as an importable Go package. HTTP(S) and raw-DER TCP TSA transports are supported. |
| Long-lived | Built on durable, independently implemented standards: RFC 5322 email, DKIM relaxed canonicalization, SHA-512, RFC 3161, CMS, ASN.1 DER, and X.509. Tokens can be cross-checked with OpenSSL. |
| Reproducible | The same message and field policy produce the same canonical bytes and message imprint. Tests use local deterministic fixtures, and generated manuals are reproducible. |
| Explicit trust | TSA responses, signed tokens, certificate chains, and message imprints are checked before output. Library callers choose authorities and trust roots explicitly. |
| Self-contained evidence | The standard timestamp token travels inside the email instead of depending on application state that may disappear later. |
Build
go build -o mailseal ./cmd/mailseal
The module root is also the importable Go package; the command remains a single executable with its manuals embedded.
Usage
Sign a message
mailseal sign <message.eml >signed.eml
mailseal sign --output-format header <message.eml # output only the signature header
mailseal sign --output-format token <message.eml >token # output raw DER token
mailseal sign --output-format response <message.eml >tsr # output full TSP response
mailseal sign --max 3 <message.eml >signed.eml # wait for 3 TSA responses
mailseal sign --header x-timestamp <message.eml # custom header field name
mailseal sign --cert=false <message.eml >signed.eml # don't embed signing cert
Inspect a signed message
mailseal verify <signed.eml
verify cryptographically checks each x-tsa-signature header: it enforces the
supported header version and field selection, validates the RFC 3161 signed
token (CMS signature and certificate chain) against the system trust store at
the token's generation time, and confirms the token's message imprint matches
the freshly normalized message. It exits nonzero if any check fails or if no
signature header is present.
Read the manual
mailseal man # mailseal(1)
mailseal man 5 # mailseal-signature(5)
When stdout is redirected, the command emits the generated roff source.
Normalize a message
Outputs the message in DKIM "relaxed" canonical form (as used for hashing before signing). Useful for debugging and cross-verification with OpenSSL.
mailseal normalize <message.eml >normalized.eml
mailseal normalize --fields from,date,subject <message.eml
Create a timestamp query
Outputs the binary ASN.1 timestamp query (for manual TSA interaction).
mailseal query <message.eml >timestamp.tsq
mailseal query --cert=false <message.eml >timestamp.tsq
Global flags
mailseal --verbose sign <message.eml # verbose output
mailseal -v sign <message.eml # short form
mailseal --debug sign <message.eml # debug output (implies verbose)
Go package
Applications can import go.schlittermann.de/heiko/mailseal directly. The
library does not select public TSAs or load system roots: callers
must explicitly choose authorities and the certificate roots they trust.
import (
"crypto/x509"
"go.schlittermann.de/heiko/mailseal"
)
roots, err := x509.SystemCertPool()
if err != nil { /* handle error */ }
authority, err := mailseal.NewAuthority("https://tsa.example.invalid/rfc3161")
if err != nil { /* handle error */ }
result, err := mailseal.Sign(ctx, message, mailseal.SignOptions{
Authorities: []mailseal.Authority{authority},
Roots: roots,
})
if err == nil { err = result.Write(output, mailseal.FormatMessage) }
For the common case of "sign and write the signed message," mailseal.SignTo
is shorthand for the Sign plus result.Write(w, mailseal.FormatMessage) pair
above; use Sign directly when you need the timestamps, tokens, or responses.
mailseal.Normalize returns the canonical bytes and mailseal.Query returns
a DER RFC 3161 request without terminal or diagnostic behavior. Verify
likewise requires an explicit root pool and returns structured verification
metadata.
How it works
Signing and independent verification operate on a normalized message. The normalization follows the DKIM "relaxed" canonicalization (RFC 6376 §3.4):
- Header field names lowercased, whitespace around colons removed
- Continuation lines unfolded, whitespace sequences collapsed
- Body: trailing whitespace per line stripped, trailing empty lines removed
- Everything normalized to CRLF line endings
A subset of header fields is signed (from, sender, date, to, cc, subject, message-id), sorted alphabetically in their canonicalized form.
The x-tsa-signature header contains a JSON payload with:
Version— format version (currently 1)Status— TSA response statusEndpoint— TSA URL that issued the timestampFields— list of signed header field namesTime— timestamp from the TSAToken— the DER-encoded timestamp token, split into 128-byte chunks
Technical details
See doc/technical-details.md for the full technical documentation, including:
- Regulatory motivation (GoBD / Unveränderbarkeit)
- Where to sign in the SMTP pipeline
- MTA integration examples (Postfix, Exim)
- Normalization, hashing, and RFC 3161 timestamps
- Trust chain and verification
Known Time Stamp Authorities
The following TSAs are compiled into mailseal. Endpoint reachability and policy links last verified 2026-07-23. The Free and Commercial use columns summarise a best-effort reading of each operator's public policy; they are not legal advice and can change without notice. Verify the current terms with the operator before relying on a service.
| TSA | Enabled | Free | Commercial use | Policy |
|---|---|---|---|---|
| rfc3161timestamp.globalsign.com/advanced | ✓ | ✓ | ✓ | TPS — de-facto public; no formal standalone ToS |
| timestamp.globalsign.com/tsa/r6advanced1 | ✓ | ✓ | ✓ | TPS — legacy Authenticode endpoint, same policy |
| freetsa.org/tsr | ✓ | ✓ | freetsa.org — explicitly free; no rate limit stated; no SLA, single operator | |
| freetsa.org:318 (TCP) | ✓ | ✓ | Native RFC 3161 raw-DER TCP service; returned a valid granted timestamp on the 2026-07-23 check; no SLA, single operator |
|
| timestamp.acs.microsoft.com | ✓ | ✓ | MS PKI — de-facto public; formally part of paid Azure Artifact Signing | |
| timestamp.apple.com/ts01 | ✓ | ? | Apple PKI — intended for Apple code signing; CA root not in third-party trust stores | |
| timestamp.digicert.com | ✓ | ✓ | DigiCert KB — de-facto public standard; no formal ToS | |
| timestamp.entrust.net/TSS/RFC3161sha2TS | ✓ | ✓ | As of the 2026-07-23 check this endpoint issues tokens signed by Sectigo's public TSA (Entrust delegated its public timestamping to Sectigo); same de-facto-public policy as the Sectigo row | |
| timestamp.sectigo.com | ✓ | ✓ | Sectigo — de-facto public; no formal standalone ToS | |
| tsa.mesign.com | ✓ | ? | MeSign TSA — 20 req/day/IP on free tier; paid plans for commercial use; jurisdiction: China | |
| time.certum.pl | ✓ | ✓ | Certum — eIDAS-qualified CA (Poland); de-facto public; no formal ToS for endpoint | |
| zeitstempel.dfn.de | ✓ | ✗ | DFN PKI — DFN members only; commercial use prohibited by bylaws. Announced academic-network IP restriction (from 2026-06-23) was not yet enforced on the 2026-07-23 check | |
| ca.signfiles.com/tsa/get.aspx | ✓ | ✗ | ca.signfiles.com/tsa — test service only: tokens are signed by a CA whose subject is literally "Do not use in production / Test"; not for real evidence |
Legend: ✓ = yes, ✗ = no, ? = unclear / no public policy found
Cross-verification with OpenSSL
Links
License
Licensed under the Apache License, Version 2.0. See LICENSE and NOTICE.