- Go 98.7%
- Shell 1.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
nagonag (Push) / nagonag (push) Successful in 3m11s
Reviewed-on: #15 |
||
| .claude/memory | ||
| .forgejo/workflows | ||
| assets | ||
| cmd/mailseal | ||
| doc | ||
| internal | ||
| list-of-free-tsa@b2af59decf | ||
| man | ||
| testdata | ||
| TSA | ||
| .gitignore | ||
| .gitmodules | ||
| .golangci.yml | ||
| AGENTS.md | ||
| api_test.go | ||
| authority.go | ||
| chain_test.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 | ||
| recipe_test.go | ||
| 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 today (the verifier reads the algorithm from the token, so future tokens are not tied to it), 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 # 3 sibling tokens in one header
mailseal sign --chain --max 2 <signed.eml >renewed.eml # one chained header with 2 siblings
mailseal sign --header x-timestamp <message.eml # custom header field name
mailseal sign --cert=false <message.eml >signed.eml # don't embed signing cert
mailseal sign --chain <signed.eml >renewed.eml # renew: fold prior x-tsa-signature(s) into the new stamp
--chain (alias --renew) requests self-chaining renewal: when the input
already carries one or more x-tsa-signature headers, the new timestamp's
imprint additionally covers those prior signature headers, so the fresh stamp
cryptographically attests that the earlier ones existed intact. This defends
against future algorithm decay by periodically re-timestamping with a
stronger hash or signature scheme over the whole prior chain. Without
--chain (the default), re-signing an already-signed message produces an
independent second stamp over the original content only, exactly as before —
chaining is opt-in so existing workflows never change unexpectedly.
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.
When a message carries a self-chained series of x-tsa-signature headers
(see --chain above), verify reconstructs the chain oldest-to-newest from
each token's authenticated generation time (never header position, so a
relay reordering the headers does not matter) and validates every link. It
reports the highest contiguous verified generation rather than claiming
full-chain validity when an interior link is broken.
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 2)Canon,CanonVersion,Fields, andFieldSet— shared advisory recipe metadataCreated— advisory local signing time, for display onlyTimestamps— one entry per accepted TSA response; each has advisoryEndpoint,Status, and duplicateTime, plus its 128-byte-chunked DERToken
One signing operation produces exactly one header, so --max produces sibling
tokens in one generation. A chained header hashes complete prior v2 header
lines; an independent header after a chain remains independent in verification.
A self-chained (--chain) generation's imprint covers the default header
fields, then the sorted canonicalized bytes of every strictly earlier
x-tsa-signature line, then the body — never the header being created (that
would be self-referential). Chain order for verification is reconstructed
from each token's authenticated generation time, not from header position.
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.