Module: MailAuth::Dkim

Defined in:
lib/mailauth/dkim.rb,
lib/mailauth/dkim/signer.rb,
lib/mailauth/dkim/algorithm.rb,
lib/mailauth/dkim/public_key.rb,
lib/mailauth/dkim/canonicalization.rb

Overview

RFC 6376. Verifies the DKIM-Signature headers on a message against the public keys their domains publish, and signs outgoing messages with the private ones.

Defined Under Namespace

Modules: Algorithm, Canonicalization, PublicKey Classes: Malformed, Signer, Verification

Constant Summary collapse

CRLF =
"\r\n".b
HASHES =

RFC 8301 §3.1 retired rsa-sha1: a verifier must not consider one valid.

{ "sha256" => OpenSSL::Digest::SHA256 }.freeze
SIGNING_ALGORITHMS =
%w[ rsa ed25519 ].freeze
KEY_TYPES =

What OpenSSL calls a key, mapped to what a= calls it.

{ "rsaEncryption" => "rsa", "ED25519" => "ed25519" }.freeze
CANONICALIZATIONS =
%w[ simple relaxed ].freeze
REQUIRED_TAGS =
%w[ v a b bh d h s ].freeze
TAG_NAME =

RFC 6376 §3.2: ALPHA *ALNUMPUNC.

/\A[a-zA-Z][a-zA-Z0-9_]*\z/n
MAXIMUM_SIGNATURES =

RFC 6376 §8.4. Each signature costs a lookup for a key its sender named, and headers are cheap to write, so only the first so many are answered.

10
MINIMUM_RSA_BITS =

Shorter RSA keys are factorable cheaply enough to prove nothing.

1024
ED25519_SPKI_PREFIX =

ed25519 keys travel as 32 raw bytes (RFC 8463 §3), so we put back the SubjectPublicKeyInfo wrapper OpenSSL insists on before reading one.

[ "302a300506032b6570032100" ].pack("H*")

Class Method Summary collapse

Class Method Details

.parse_tags(text) ⇒ Object

RFC 6376 §3.2. A malformed tag name, a repeated one, or an empty entry invalidates the whole list rather than the one tag: reading the rest would take a forged tag as read beside the one silently dropped. Values fold across lines, so callers strip whitespace themselves on the tags where it matters.



72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/mailauth/dkim.rb', line 72

def self.parse_tags(text)
  specs = text.split(";", -1)
  specs.pop if specs.last&.strip&.empty?   # the tag-list's optional trailing ";"

  specs.each_with_object({}) do |spec, tags|
    name, value = spec.split("=", 2)
    name = name.to_s.strip

    return nil unless value && name.match?(TAG_NAME) && !tags.key?(name)

    tags[name] = value.strip
  end
end

.sign(raw, domain:, selector:, key:, headers: nil, oversign: Signer::OVERSIGNED_HEADERS, expires_in: nil, now: Time.now) ⇒ Object



63
64
65
# File 'lib/mailauth/dkim.rb', line 63

def self.sign(raw, domain:, selector:, key:, headers: nil, oversign: Signer::OVERSIGNED_HEADERS, expires_in: nil, now: Time.now)
  Signer.new(raw, domain:, selector:, key:, headers:, oversign:, expires_in:, now:).signed_message
end

.verify(message, resolver: Resolver.new, now: Time.now) ⇒ Object



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/mailauth/dkim.rb', line 44

def self.verify(message, resolver: Resolver.new, now: Time.now)
  fields = message.fields_named("dkim-signature")

  signatures = fields.first(MAXIMUM_SIGNATURES).map do |field|
    Verification.new(message, field, resolver:, now:).result
  end

  # The ones left over are reported rather than dropped: a verifier that
  # quietly ignores a signature says a message carried fewer than it did.
  if fields.empty?
    signatures << Signature.new(status: Status::NONE, comment: "message not signed")
  elsif fields.length > MAXIMUM_SIGNATURES
    signatures << Signature.new(status: Status::PERMERROR,
      comment: "#{fields.length - MAXIMUM_SIGNATURES} more signatures not evaluated")
  end

  DkimResult.new(signatures:)
end