sign RFC5322 messages with a trusted time stamp
  • Go 98.3%
  • Shell 1.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Heiko Schlittermann (ai) 6de6cf5398 chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5
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.
2026-07-23 20:56:39 +02:00
.claude/memory chore: update lint and repository configuration 2026-07-21 16:37:14 +02:00
assets refactor!: rename module and command to mailseal 2026-07-22 16:57:46 +02:00
cmd/mailseal chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
doc feat!: rename x-signature header to x-tsa-signature ai:claude-sonnet-4-5 2026-07-23 19:49:49 +02:00
internal chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
list-of-free-tsa@b2af59decf move list-of-free-tsa 2024-12-21 21:49:06 +01:00
man chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
testdata feat!: rename x-signature header to x-tsa-signature ai:claude-sonnet-4-5 2026-07-23 19:49:49 +02:00
TSA chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
.gitignore refactor!: rename module and command to mailseal 2026-07-22 16:57:46 +02:00
.gitmodules refactor:mail 2024-12-21 21:49:05 +01:00
.golangci.yml chore: update lint and repository configuration 2026-07-21 16:37:14 +02:00
AGENTS.md feat!: rename x-signature header to x-tsa-signature ai:claude-sonnet-4-5 2026-07-23 19:49:49 +02:00
api_test.go chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
authority.go chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
doc.go chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
event.go chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
fuzz_verify_test.go chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
go.mod refactor!: rename module and command to mailseal 2026-07-22 16:57:46 +02:00
go.sum feat: add embedded manual pages 2026-07-21 17:26:35 +02:00
header.go chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
LICENSE chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
normalize.go chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
NOTICE chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
public_test.go chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
query.go chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
README.md chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
result.go chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
sign.go chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
sign_e2e_test.go chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
sign_test_helpers_test.go chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
sign_verify_regression_test.go chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
token.go chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00
verify.go chore: add Apache-2.0 LICENSE, NOTICE and SPDX headers ai:claude-sonnet-4-5 2026-07-23 20:56:39 +02:00

Go Reference

mailseal — durable proof of existence for email; simple, portable, long-lived, and reproducible

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 status
  • Endpoint — TSA URL that issued the timestamp
  • Fields — list of signed header field names
  • Time — timestamp from the TSA
  • Token — 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

See doc/technical-details.md.

License

Licensed under the Apache License, Version 2.0. See LICENSE and NOTICE.