Documentation
¶
Overview ¶
Package arc implements RFC 8617, the Authenticated Received Chain (ARC) protocol.
ARC provides an authenticated "chain of custody" for email messages, allowing each entity that handles the message to see what entities handled it before and what the message's authentication assessment was at each step.
Validating an ARC Chain ¶
To validate an ARC chain on an incoming message:
import (
"context"
"io"
"github.com/schmidtw/arc"
)
func validateMessage(message io.Reader) error {
v, err := arc.NewValidator() // uses net.DefaultResolver
if err != nil {
return err
}
present, err := v.Validate(context.Background(), message)
if err != nil {
return err // chain validation failed
}
if !present {
// No ARC headers present
}
// ARC chain validated successfully
return nil
}
Signing a Message (Creating a New ARC Set) ¶
To add an ARC Set to a message:
import (
"context"
"crypto"
"github.com/schmidtw/arc"
)
func signMessage(message []byte, privateKey crypto.Signer) ([]byte, error) {
signer, err := arc.NewSigner(privateKey, "sel._domainkey.example.org")
if err != nil {
return nil, err
}
return signer.SignBytes(context.Background(), message, "spf=pass; dkim=pass")
}
The Sign method validates any existing ARC chain before adding a new set. If validation succeeds, the new set is marked as passing. If validation fails, the new set is marked as failing. Signing is refused if the most recent set in the chain was already marked as failing.
To customize validation behavior, provide a validator via WithValidator:
validator, err := arc.NewValidator(arc.WithMinRSAKeyBits(2048))
if err != nil {
return nil, err
}
signer, err := arc.NewSigner(privateKey, "sel._domainkey.example.org",
arc.WithValidator(validator))
if err != nil {
return nil, err
}
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrInvalidSignature = errors.New("invalid signature")
ErrInvalidSignature is returned when a cryptographic signature verification fails during ARC chain validation. Use errors.Is to check for this error.
var ErrRSAKeyTooSmall = errors.New("RSA key size is too small")
ErrRSAKeyTooSmall is returned when an RSA key is smaller than the configured minimum size during validation or signing. Use errors.Is to check for this error.
Functions ¶
This section is empty.
Types ¶
type HeaderField ¶
type HeaderField string
HeaderField identifies a message header field for inclusion in the ARC-Message-Signature. Standard values are provided as constants. Custom headers can be used by converting a string: HeaderField("X-My-Header").
const ( HeaderFrom HeaderField = "From" HeaderTo HeaderField = "To" HeaderCc HeaderField = "Cc" HeaderSubject HeaderField = "Subject" HeaderDate HeaderField = "Date" HeaderReplyTo HeaderField = "Reply-To" HeaderInReplyTo HeaderField = "In-Reply-To" HeaderReferences HeaderField = "References" HeaderMessageID HeaderField = "Message-ID" HeaderMIMEVersion HeaderField = "MIME-Version" HeaderContentType HeaderField = "Content-Type" HeaderContentTransferEncoding HeaderField = "Content-Transfer-Encoding" HeaderResentDate HeaderField = "Resent-Date" HeaderResentFrom HeaderField = "Resent-From" HeaderResentTo HeaderField = "Resent-To" HeaderResentCc HeaderField = "Resent-Cc" HeaderListID HeaderField = "List-Id" HeaderListHelp HeaderField = "List-Help" HeaderListUnsubscribe HeaderField = "List-Unsubscribe" HeaderListSubscribe HeaderField = "List-Subscribe" HeaderListPost HeaderField = "List-Post" HeaderListOwner HeaderField = "List-Owner" HeaderListArchive HeaderField = "List-Archive" HeaderDKIMSignature HeaderField = "DKIM-Signature" )
Standard header fields recommended for signing.
const ( // ARC and authentication headers must not be included in signatures. HeaderArcSeal HeaderField = "ARC-Seal" HeaderArcMessageSignature HeaderField = "ARC-Message-Signature" HeaderArcAuthenticationResults HeaderField = "ARC-Authentication-Results" HeaderAuthenticationResults HeaderField = "Authentication-Results" // Headers commonly modified or removed in transit should not be signed. HeaderReturnPath HeaderField = "Return-Path" HeaderReceived HeaderField = "Received" HeaderComments HeaderField = "Comments" HeaderKeywords HeaderField = "Keywords" )
Headers that must not or should not be signed.
type Option ¶
type Option interface {
SignerOption
ValidatorOption
}
Option is an option that can be passed to both NewValidator and NewSigner.
func WithMinRSAKeyBits ¶
WithMinRSAKeyBits sets the minimum accepted RSA key size in bits. This option can be passed to both NewValidator and NewSigner.
For validators, this controls the minimum key size accepted when validating other parties' signatures. Default is 1024 bits per RFC 8301 (which updates RFC 6376 Section 3.3.3): verifiers MUST accept 1024-bit to 4096-bit keys.
For signers, this controls the minimum key size for the signer's own private key. Default is 2048 bits per RFC 8301, which states that signers SHOULD use at least 2048 bits.
Values less than 1024 result in an error.
See https://www.rfc-editor.org/rfc/rfc8301 and https://www.rfc-editor.org/rfc/rfc6376#section-3.3.3 for details.
func WithResolver ¶
WithResolver sets a custom Resolver for DNS TXT record lookups. If not set, net.DefaultResolver is used. This option can be passed to both NewValidator and NewSigner.
type Resolver ¶
Resolver looks up DNS TXT records. The standard library's *net.Resolver satisfies this interface.
type Signer ¶
type Signer struct {
// contains filtered or unexported fields
}
Signer adds new ARC Sets to email messages.
func NewSigner ¶
NewSigner creates a new Signer with the given key and domain key FQDN.
The key is the private key used for signing. The signing algorithm is inferred from the key type (RSA or Ed25519).
The domainKey is the DNS domain key FQDN where verifiers look up the corresponding public key:
<selector>._domainkey.<domain>
For example, "2024._domainkey.example.com".
A validator is used to validate existing ARC chains before adding a new set. This determines whether the new ARC-Seal will have cv=pass or cv=fail. If no validator is provided via WithValidator, a default validator with standard settings is created.
By default, the default signed headers are used, which include common message headers (From, To, Subject, Date, etc.). Use WithSignedHeaders to override this set. If no authentication server ID is set via WithAuthServID, it defaults to the domain. If no resolver is set, net.DefaultResolver is used.
The maximum number of ARC Sets is fixed at 50 per RFC 8617. The RFC defines instance values (i=) as ranging from 1 to 50, making this an absolute limit in the protocol specification.
func (*Signer) Sign ¶
Sign creates a new ARC Set and prepends it to the message. The message should be a raw email message (RFC 5322 format). It returns the message with the new ARC headers prepended. The authResults parameter is the authentication results string for the ARC-Authentication-Results header (e.g., "spf=pass; dkim=pass").
type SignerOption ¶
type SignerOption interface {
// contains filtered or unexported methods
}
SignerOption configures a Signer.
func WithAuthServID ¶
func WithAuthServID(id string) SignerOption
WithAuthServID sets the authentication server identifier for the ARC-Authentication-Results header. This identifies the organization that performed authentication checks on the message. If not set, defaults to the signing domain.
See https://www.rfc-editor.org/rfc/rfc8601#section-2.5 for guidance on choosing an appropriate value.
func WithSignedHeaders ¶
func WithSignedHeaders(headers ...HeaderField) SignerOption
WithSignedHeaders sets the message header fields whose values will be covered by the ARC-Message-Signature. Use the HeaderField constants for standard fields, or convert custom header names: HeaderField("X-Custom-Header").
func WithTimestamp ¶
func WithTimestamp(ts time.Time) SignerOption
WithTimestamp sets a fixed signing timestamp. If not set or zero, the current time is used at signing time.
func WithValidator ¶
func WithValidator(v *Validator) SignerOption
WithValidator sets a custom Validator for the signer to use when validating existing ARC chains before adding a new set.
If not provided, a default validator is created with:
- net.DefaultResolver for DNS lookups
- 1024-bit minimum RSA key size (per RFC 8301)
- 50 maximum ARC sets
You can create a custom validator to control validation behavior:
v, err := arc.NewValidator(
arc.WithMinRSAKeyBits(2048),
arc.WithResolver(customResolver),
)
if err != nil {
return err
}
signer, err := arc.NewSigner(key, domainKey, arc.WithValidator(v))
if err != nil {
return err
}
type Validator ¶
type Validator struct {
// contains filtered or unexported fields
}
Validator validates ARC chains on email messages.
func NewValidator ¶
func NewValidator(opts ...ValidatorOption) (*Validator, error)
NewValidator creates a new Validator. If no Resolver is provided via WithResolver, net.DefaultResolver is used. By default, the minimum accepted RSA key size is 1024 bits. Use WithMinRSAKeyBits to override.
The DNS key cache is bounded by default to 1000 entries using LRU eviction. Use WithMaxCacheSize to adjust the limit or disable caching.
The maximum number of ARC Sets is fixed at 50 per RFC 8617. The RFC defines instance values (i=) as ranging from 1 to 50, making this an absolute limit in the protocol specification.
type ValidatorOption ¶
type ValidatorOption interface {
// contains filtered or unexported methods
}
ValidatorOption configures a Validator.
func WithMaxCacheSize ¶
func WithMaxCacheSize(size int) ValidatorOption
WithMaxCacheSize sets the maximum number of parsed DNS public keys to cache. This option can be passed to NewValidator.
The cache stores parsed key records and built verifier functions. DNS lookups are always performed to fetch the current TXT record; the cache avoids the expensive parsing and cryptographic operations when the DNS record content hasn't changed. This provides content-based cache invalidation - if a key is rotated, the cache detects the change and rebuilds the verifier.
The cache uses LRU (Least Recently Used) eviction when the limit is reached.
Accepted values:
- 0: Disables caching (always parses and builds verifiers from DNS records)
- Positive integers: Cache up to that many entries with LRU eviction
- -1 or any negative value: Unlimited cache size (all negative values are normalized to -1)
Warning: Unlimited cache (-1) is not recommended for long-running services as it can lead to unbounded memory growth.
Default is 1000 entries, which should be sufficient for most use cases while preventing unbounded memory growth in long-running validators.
Note: The cache does not respect DNS TTL values because Go's net.Resolver does not expose TTL information from DNS responses. Cached entries are evicted only by LRU policy or when the DNS record content changes.