dkim 0.1.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/test.yml +20 -0
- data/.gitignore +1 -0
- data/Appraisals +7 -0
- data/CHANGELOG.md +28 -3
- data/Gemfile +5 -1
- data/README.md +52 -61
- data/Rakefile +12 -0
- data/bin/dkimsign.rb +0 -0
- data/dkim.gemspec +5 -0
- data/gemfiles/mail_2.6.gemfile +11 -0
- data/gemfiles/mail_2.7.gemfile +11 -0
- data/lib/dkim/canonicalized_headers.rb +26 -0
- data/lib/dkim/dkim_header.rb +21 -12
- data/lib/dkim/encodings/base64.rb +12 -0
- data/lib/dkim/encodings/dkim_quoted_printable.rb +19 -0
- data/lib/dkim/encodings/plain_text.rb +8 -0
- data/lib/dkim/encodings.rb +3 -0
- data/lib/dkim/header.rb +7 -0
- data/lib/dkim/interceptor.rb +13 -1
- data/lib/dkim/options.rb +84 -0
- data/lib/dkim/signed_mail.rb +31 -56
- data/lib/dkim/tag_value_list.rb +28 -0
- data/lib/dkim/version.rb +1 -1
- data/lib/dkim.rb +4 -8
- data/lib/mail/dkim_field.rb +4 -4
- data/test/dkim/canonicalization_test.rb +80 -0
- data/test/dkim/canonicalized_headers_test.rb +51 -0
- data/test/dkim/dkim_header_test.rb +43 -0
- data/test/dkim/encodings_test.rb +24 -0
- data/test/dkim/interceptor_test.rb +129 -0
- data/test/dkim/options_test.rb +40 -0
- data/test/dkim/signed_mail_test.rb +112 -0
- data/test/dkim/tag_value_list_test.rb +28 -0
- data/test/test_helper.rb +36 -4
- metadata +95 -18
- data/lib/dkim/header_list.rb +0 -19
- data/test/canonicalization_test.rb +0 -78
data/lib/dkim/signed_mail.rb
CHANGED
@@ -3,70 +3,50 @@ require 'openssl'
|
|
3
3
|
require 'dkim/body'
|
4
4
|
require 'dkim/dkim_header'
|
5
5
|
require 'dkim/header'
|
6
|
-
require 'dkim/
|
6
|
+
require 'dkim/options'
|
7
|
+
require 'dkim/canonicalized_headers'
|
7
8
|
|
8
9
|
module Dkim
|
9
10
|
class SignedMail
|
11
|
+
include Options
|
12
|
+
|
13
|
+
# A new instance of SignedMail
|
14
|
+
#
|
15
|
+
# @param [String,#to_s] message mail message to be signed
|
16
|
+
# @param [Hash] options hash of options for signing. Defaults are taken from {Dkim}. See {Options} for details.
|
10
17
|
def initialize message, options={}
|
11
18
|
message = message.to_s.gsub(/\r?\n/, "\r\n")
|
12
19
|
headers, body = message.split(/\r?\n\r?\n/, 2)
|
13
|
-
@
|
20
|
+
@original_message = message
|
21
|
+
@headers = Header.parse headers
|
14
22
|
@body = Body.new body
|
15
23
|
|
16
|
-
|
17
|
-
@
|
18
|
-
@selector = options[:selector]
|
19
|
-
@time = options[:time]
|
20
|
-
@signing_algorithm = options[:signing_algorithm]
|
21
|
-
@private_key = options[:private_key]
|
22
|
-
@header_canonicalization = options[:header_canonicalization]
|
23
|
-
@body_canonicalization = options[:body_canonicalization]
|
24
|
+
# default options from Dkim.options
|
25
|
+
@options = Dkim.options.merge(options)
|
24
26
|
end
|
25
27
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
def private_key= key
|
30
|
-
key = OpenSSL::PKey::RSA.new(key) if key.is_a?(String)
|
31
|
-
@private_key = key
|
32
|
-
end
|
33
|
-
def private_key
|
34
|
-
@private_key || Dkim::private_key
|
35
|
-
end
|
36
|
-
def signing_algorithm
|
37
|
-
@signing_algorithm || Dkim::signing_algorithm
|
38
|
-
end
|
39
|
-
def signable_headers
|
40
|
-
@signable_headers || Dkim::signable_headers
|
41
|
-
end
|
42
|
-
def domain
|
43
|
-
@domain || Dkim::domain
|
44
|
-
end
|
45
|
-
def selector
|
46
|
-
@selector || Dkim::selector
|
47
|
-
end
|
48
|
-
def time
|
49
|
-
@time
|
50
|
-
end
|
51
|
-
def header_canonicalization
|
52
|
-
@header_canonicalization || Dkim::header_canonicalization
|
53
|
-
end
|
54
|
-
def body_canonicalization
|
55
|
-
@body_canonicalization || Dkim::body_canonicalization
|
28
|
+
def canonicalized_headers
|
29
|
+
CanonicalizedHeaders.new(@headers, signed_headers)
|
56
30
|
end
|
57
31
|
|
32
|
+
# @return [Array<String>] lowercased names of headers in the order they are signed
|
58
33
|
def signed_headers
|
59
|
-
|
34
|
+
@headers.map(&:relaxed_key).select do |key|
|
35
|
+
signable_headers.map(&:downcase).include?(key)
|
36
|
+
end
|
60
37
|
end
|
38
|
+
|
39
|
+
# @return [String] Signed headers of message in their canonical forms
|
61
40
|
def canonical_header
|
62
|
-
|
63
|
-
@headers[key].to_s(header_canonicalization) + "\r\n"
|
64
|
-
end.join
|
41
|
+
canonicalized_headers.to_s(header_canonicalization)
|
65
42
|
end
|
43
|
+
|
44
|
+
# @return [String] Body of message in its canonical form
|
66
45
|
def canonical_body
|
67
46
|
@body.to_s(body_canonicalization)
|
68
47
|
end
|
69
48
|
|
49
|
+
# @return [DkimHeader] Constructed signature for the mail message
|
70
50
|
def dkim_header
|
71
51
|
dkim_header = DkimHeader.new
|
72
52
|
|
@@ -79,41 +59,36 @@ module Dkim
|
|
79
59
|
dkim_header['a'] = signing_algorithm
|
80
60
|
dkim_header['c'] = "#{header_canonicalization}/#{body_canonicalization}"
|
81
61
|
dkim_header['d'] = domain
|
62
|
+
dkim_header['i'] = identity if identity
|
82
63
|
dkim_header['q'] = 'dns/txt'
|
83
64
|
dkim_header['s'] = selector
|
84
65
|
dkim_header['t'] = (time || Time.now).to_i
|
66
|
+
dkim_header['x'] = expire.to_i if expire
|
85
67
|
|
86
68
|
# Add body hash and blank signature
|
87
|
-
dkim_header['bh']=
|
69
|
+
dkim_header['bh']= digest_alg.digest(canonical_body)
|
88
70
|
dkim_header['h'] = signed_headers.join(':')
|
89
71
|
dkim_header['b'] = ''
|
90
72
|
|
91
73
|
# Calculate signature based on intermediate signature header
|
92
74
|
headers = canonical_header
|
93
75
|
headers << dkim_header.to_s(header_canonicalization)
|
94
|
-
|
95
|
-
dkim_header['b'] = signature
|
76
|
+
dkim_header['b'] = private_key.sign(digest_alg, headers)
|
96
77
|
|
97
78
|
dkim_header
|
98
79
|
end
|
99
80
|
|
81
|
+
# @return [String] Message combined with calculated dkim header signature
|
100
82
|
def to_s
|
101
|
-
|
102
|
-
headers = @headers.to_a + [dkim_header]
|
103
|
-
headers.map(&:to_s).join("\r\n") +
|
104
|
-
"\r\n\r\n" +
|
105
|
-
@body.to_s
|
83
|
+
dkim_header.to_s + "\r\n" + @original_message
|
106
84
|
end
|
107
85
|
|
108
86
|
private
|
109
|
-
def base64_encode data
|
110
|
-
[data].pack('m0*').gsub("\n",'')
|
111
|
-
end
|
112
87
|
def digest_alg
|
113
88
|
case signing_algorithm
|
114
89
|
when 'rsa-sha1'
|
115
90
|
OpenSSL::Digest::SHA1.new
|
116
|
-
when 'rsa-sha256'
|
91
|
+
when 'rsa-sha256'
|
117
92
|
OpenSSL::Digest::SHA256.new
|
118
93
|
else
|
119
94
|
raise "Unknown digest algorithm: '#{signing_algorithm}'"
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Dkim
|
2
|
+
class TagValueList
|
3
|
+
def initialize values={}
|
4
|
+
@keys = values.keys
|
5
|
+
@values = values.dup
|
6
|
+
end
|
7
|
+
def to_s
|
8
|
+
@keys.map do |k|
|
9
|
+
"#{k}=#{@values[k]}"
|
10
|
+
end.join('; ')
|
11
|
+
end
|
12
|
+
def [] k
|
13
|
+
@values[k]
|
14
|
+
end
|
15
|
+
def []= k, v
|
16
|
+
@keys << k unless self[k]
|
17
|
+
@values[k] = v
|
18
|
+
end
|
19
|
+
def self.parse string
|
20
|
+
list = new
|
21
|
+
string.split(';').each do |keyval|
|
22
|
+
key, value = keyval.split('=', 2)
|
23
|
+
list[key.strip] = value.strip
|
24
|
+
end
|
25
|
+
list
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/dkim/version.rb
CHANGED
data/lib/dkim.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
|
2
2
|
require 'dkim/signed_mail'
|
3
|
+
require 'dkim/options'
|
3
4
|
require 'dkim/interceptor'
|
4
5
|
|
5
6
|
module Dkim
|
@@ -14,13 +15,7 @@ module Dkim
|
|
14
15
|
List-Post List-Owner List-Archive}
|
15
16
|
|
16
17
|
class << self
|
17
|
-
|
18
|
-
|
19
|
-
attr_reader :private_key
|
20
|
-
def private_key= key
|
21
|
-
key = OpenSSL::PKey::RSA.new(key) if key.is_a?(String)
|
22
|
-
@private_key = key
|
23
|
-
end
|
18
|
+
include Dkim::Options
|
24
19
|
|
25
20
|
def sign message, options={}
|
26
21
|
SignedMail.new(message, options).to_s
|
@@ -28,8 +23,9 @@ module Dkim
|
|
28
23
|
end
|
29
24
|
end
|
30
25
|
|
31
|
-
Dkim::signable_headers = Dkim::DefaultHeaders
|
26
|
+
Dkim::signable_headers = Dkim::DefaultHeaders.dup
|
32
27
|
Dkim::domain = nil
|
28
|
+
Dkim::identity = nil
|
33
29
|
Dkim::selector = nil
|
34
30
|
Dkim::signing_algorithm = 'rsa-sha256'
|
35
31
|
Dkim::private_key = nil
|
data/lib/mail/dkim_field.rb
CHANGED
@@ -9,17 +9,17 @@ module Mail
|
|
9
9
|
|
10
10
|
def initialize(value = nil, charset = 'utf-8')
|
11
11
|
self.charset = charset
|
12
|
-
|
12
|
+
value = strip_field(FIELD_NAME, value) if respond_to?(:strip_field)
|
13
|
+
super(CAPITALIZED_FIELD, value, charset)
|
13
14
|
self
|
14
15
|
end
|
15
16
|
|
16
17
|
def encoded
|
17
|
-
"#{name}:#{value}\n"
|
18
|
+
"#{name}:#{value}\r\n"
|
18
19
|
end
|
19
20
|
|
20
21
|
def decoded
|
21
|
-
"#{name}:#{value}\n"
|
22
|
+
"#{name}:#{value}\r\n"
|
22
23
|
end
|
23
24
|
end
|
24
25
|
end
|
25
|
-
|
@@ -0,0 +1,80 @@
|
|
1
|
+
|
2
|
+
require 'test_helper'
|
3
|
+
|
4
|
+
module Dkim
|
5
|
+
class CanonicalizationTest < Minitest::Test
|
6
|
+
# from section 3.4.6 of RFC 6376
|
7
|
+
def setup
|
8
|
+
@input = <<-eos.rfc_format
|
9
|
+
A: <SP> X <CRLF>
|
10
|
+
B <SP> : <SP> Y <HTAB><CRLF>
|
11
|
+
<HTAB> Z <SP><SP><CRLF>
|
12
|
+
<CRLF>
|
13
|
+
<SP> C <SP><CRLF>
|
14
|
+
D <SP><HTAB><SP> E <CRLF>
|
15
|
+
<CRLF>
|
16
|
+
<CRLF>
|
17
|
+
eos
|
18
|
+
@mail = SignedMail.new(@input)
|
19
|
+
@mail.signable_headers = ['A', 'B']
|
20
|
+
end
|
21
|
+
def test_relaxed_header
|
22
|
+
@mail.header_canonicalization = 'relaxed'
|
23
|
+
expected_header = <<-eos.rfc_format
|
24
|
+
a:X <CRLF>
|
25
|
+
b:Y <SP> Z <CRLF>
|
26
|
+
eos
|
27
|
+
assert_equal expected_header, @mail.canonical_header
|
28
|
+
end
|
29
|
+
def test_relaxed_body
|
30
|
+
@mail.body_canonicalization = 'relaxed'
|
31
|
+
expected_body = <<-eos.rfc_format
|
32
|
+
<SP> C <CRLF>
|
33
|
+
D <SP> E <CRLF>
|
34
|
+
eos
|
35
|
+
assert_equal expected_body, @mail.canonical_body
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_simple_header
|
39
|
+
@mail.header_canonicalization = 'simple'
|
40
|
+
expected_header = <<-eos.rfc_format
|
41
|
+
A: <SP> X <CRLF>
|
42
|
+
B <SP> : <SP> Y <HTAB><CRLF>
|
43
|
+
<HTAB> Z <SP><SP><CRLF>
|
44
|
+
eos
|
45
|
+
assert_equal expected_header, @mail.canonical_header
|
46
|
+
end
|
47
|
+
def test_simple_body
|
48
|
+
@mail.body_canonicalization = 'simple'
|
49
|
+
expected_body = <<-eos.rfc_format
|
50
|
+
<SP> C <SP><CRLF>
|
51
|
+
D <SP><HTAB><SP> E <CRLF>
|
52
|
+
eos
|
53
|
+
assert_equal expected_body, @mail.canonical_body
|
54
|
+
end
|
55
|
+
|
56
|
+
# from errata: empty bodies
|
57
|
+
def test_simple_empty_body
|
58
|
+
@mail = SignedMail.new("test: test\r\n\r\n")
|
59
|
+
@mail.body_canonicalization = 'simple'
|
60
|
+
|
61
|
+
assert_equal "\r\n", @mail.canonical_body
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_relaxed_empty_body
|
65
|
+
@mail = SignedMail.new("test: test\r\n\r\n")
|
66
|
+
@mail.body_canonicalization = 'relaxed'
|
67
|
+
|
68
|
+
assert_equal "", @mail.canonical_body
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_relaxed_errata_1384
|
72
|
+
body = "testing<crlf><sp><sp><cr><lf><cr><lf>".rfc_format
|
73
|
+
@mail = SignedMail.new("test: test\r\n\r\n#{body}")
|
74
|
+
@mail.body_canonicalization = 'relaxed'
|
75
|
+
|
76
|
+
assert_equal "testing\r\n", @mail.canonical_body
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
@@ -0,0 +1,51 @@
|
|
1
|
+
|
2
|
+
require 'test_helper'
|
3
|
+
|
4
|
+
module Dkim
|
5
|
+
class CanonicalizedHeadersTest < Minitest::Test
|
6
|
+
def test_maintains_order
|
7
|
+
headers = "ABCD".chars.map {|c| Header.new(c, c) }
|
8
|
+
header_keys = headers.map &:relaxed_key
|
9
|
+
header_keys.permutation.each do |signed_headers|
|
10
|
+
ch = CanonicalizedHeaders.new(headers, signed_headers)
|
11
|
+
assert_equal signed_headers, ch.map(&:relaxed_key)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_repeated_headers
|
16
|
+
headers = [
|
17
|
+
Header.new('A', '1'),
|
18
|
+
Header.new('B', '2'),
|
19
|
+
Header.new('C', '3'),
|
20
|
+
Header.new('A', '4'),
|
21
|
+
Header.new('D', '5')
|
22
|
+
]
|
23
|
+
ch = CanonicalizedHeaders.new(headers, %w{A A B C D})
|
24
|
+
assert_equal %w{4 1 2 3 5}, ch.map(&:value)
|
25
|
+
assert_equal <<-eos.rfc_format, ch.to_s('simple')
|
26
|
+
A:4<CRLF>
|
27
|
+
A:1<CRLF>
|
28
|
+
B:2<CRLF>
|
29
|
+
C:3<CRLF>
|
30
|
+
D:5<CRLF>
|
31
|
+
eos
|
32
|
+
end
|
33
|
+
|
34
|
+
# missing headers should be ignored
|
35
|
+
def test_missing_headers
|
36
|
+
headers = [
|
37
|
+
Header.new('A', '1'),
|
38
|
+
Header.new('B', '2'),
|
39
|
+
Header.new('C', '3'),
|
40
|
+
]
|
41
|
+
ch = CanonicalizedHeaders.new(headers, %w{A A B C})
|
42
|
+
assert_equal %w{a b c}, ch.map(&:relaxed_key)
|
43
|
+
assert_equal <<-eos.rfc_format, ch.to_s('simple')
|
44
|
+
A:1<CRLF>
|
45
|
+
B:2<CRLF>
|
46
|
+
C:3<CRLF>
|
47
|
+
eos
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
|
2
|
+
require 'test_helper'
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module Dkim
|
6
|
+
class DkimHeaderTest < Minitest::Test
|
7
|
+
def setup
|
8
|
+
@header = DkimHeader.new
|
9
|
+
|
10
|
+
# from Appendix A of RFC 6376
|
11
|
+
@header['v'] = '1'
|
12
|
+
@header['a'] = 'rsa-sha256'
|
13
|
+
@header['s'] = 'brisbane'
|
14
|
+
@header['d'] = 'example.com'
|
15
|
+
@header['c'] = 'simple/simple'
|
16
|
+
@header['q'] = 'dns/txt'
|
17
|
+
@header['i'] = 'joe@football.example.com'
|
18
|
+
@header['h'] = 'Received : From : To : Subject : Date : Message-ID'
|
19
|
+
@header['bh']= Base64.decode64 '2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8='
|
20
|
+
@header['b'] = Base64.decode64 'AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHutKVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV4bmp/YzhwvcubU4='
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_correct_format
|
24
|
+
header = @header.to_s
|
25
|
+
|
26
|
+
# result from RFC 6376 minus trailing ';'
|
27
|
+
expected = %{
|
28
|
+
DKIM-Signature: v=1; a=rsa-sha256; s=brisbane; d=example.com;
|
29
|
+
c=simple/simple; q=dns/txt; i=joe@football.example.com;
|
30
|
+
h=Received : From : To : Subject : Date : Message-ID;
|
31
|
+
bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
|
32
|
+
b=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB
|
33
|
+
4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHut
|
34
|
+
KVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV
|
35
|
+
4bmp/YzhwvcubU4=
|
36
|
+
}
|
37
|
+
|
38
|
+
# compare removing whitespace
|
39
|
+
assert_equal expected.gsub(/\s/,''), header.gsub(/\s/,'')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Dkim
|
2
|
+
class EncodingsTest < Minitest::Test
|
3
|
+
def test_plain_text
|
4
|
+
@encoder = Encodings::PlainText.new
|
5
|
+
assert_equal 'testing123', @encoder.encode('testing123')
|
6
|
+
assert_equal 'testing123', @encoder.decode('testing123')
|
7
|
+
end
|
8
|
+
def test_base64
|
9
|
+
@encoder = Encodings::Base64.new
|
10
|
+
assert_equal 'dGVzdGluZzEyMw==', @encoder.encode('testing123')
|
11
|
+
assert_equal 'testing123', @encoder.decode('dGVzdGluZzEyMw==')
|
12
|
+
end
|
13
|
+
def test_quoted_printable
|
14
|
+
@encoder = Encodings::DkimQuotedPrintable.new
|
15
|
+
assert_equal 'testing123', @encoder.encode('testing123')
|
16
|
+
assert_equal 'testing123', @encoder.decode('testing123')
|
17
|
+
|
18
|
+
encoded = 'From:foo@eng.example.net|To:joe@example.com|Subject:demo=20run|Date:July=205,=202005=203:44:08=20PM=20-0700'
|
19
|
+
decoded = 'From:foo@eng.example.net|To:joe@example.com|Subject:demo run|Date:July 5, 2005 3:44:08 PM -0700'
|
20
|
+
assert_equal encoded, @encoder.encode(decoded)
|
21
|
+
assert_equal decoded, @encoder.decode(encoded)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
require 'mail'
|
4
|
+
|
5
|
+
module Dkim
|
6
|
+
SIGNEDMAIL = <<-eos
|
7
|
+
DKIM-Signature: v=1; a=rsa-sha256; s=brisbane; d=example.com;
|
8
|
+
c=simple/simple; q=dns/txt; i=joe@football.example.com;
|
9
|
+
h=Received : From : To : Subject : Date : Message-ID;
|
10
|
+
bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
|
11
|
+
b=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB
|
12
|
+
4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHut
|
13
|
+
KVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV
|
14
|
+
4bmp/YzhwvcubU4=;
|
15
|
+
Received: from client1.football.example.com [192.0.2.1]
|
16
|
+
by submitserver.example.com with SUBMISSION;
|
17
|
+
Fri, 11 Jul 2003 21:01:54 -0700 (PDT)
|
18
|
+
From: Joe SixPack <joe@football.example.com>
|
19
|
+
To: Suzie Q <suzie@shopping.example.net>
|
20
|
+
Subject: Is dinner ready?
|
21
|
+
Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT)
|
22
|
+
Message-ID: <20030712040037.46341.5F8J@football.example.com>
|
23
|
+
DKIM-Signature: v=1; a=rsa-sha256; s=brisbane; d=example.com;
|
24
|
+
c=simple/simple; q=dns/txt; i=joe@football.example.com;
|
25
|
+
h=Received : From : To : Subject : Date : Message-ID;
|
26
|
+
bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
|
27
|
+
b=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB
|
28
|
+
4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHut
|
29
|
+
KVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV
|
30
|
+
4bmp/YzhwvcubU4=;
|
31
|
+
|
32
|
+
Hi.
|
33
|
+
|
34
|
+
We lost the game. Are you hungry yet?
|
35
|
+
|
36
|
+
Joe.
|
37
|
+
eos
|
38
|
+
|
39
|
+
class InterceptorTest < Minitest::Test
|
40
|
+
def setup
|
41
|
+
@original_options = Dkim.options.dup
|
42
|
+
|
43
|
+
# ensure time does not change
|
44
|
+
Dkim.time = Time.now
|
45
|
+
|
46
|
+
mail = EXAMPLEEMAIL.dup
|
47
|
+
|
48
|
+
@mail = Mail.new(mail)
|
49
|
+
end
|
50
|
+
|
51
|
+
def teardown
|
52
|
+
Dkim.options = @original_options
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_header_with_relaxed
|
56
|
+
Dkim.header_canonicalization = 'relaxed'
|
57
|
+
Dkim.body_canonicalization = 'relaxed'
|
58
|
+
Dkim.signing_algorithm = 'rsa-sha256'
|
59
|
+
Dkim.identity = '@example.com'
|
60
|
+
Interceptor.delivering_email(@mail)
|
61
|
+
dkim_header = @mail['DKIM-Signature']
|
62
|
+
|
63
|
+
assert dkim_header
|
64
|
+
assert_includes dkim_header.to_s, 'rsa-sha256'
|
65
|
+
assert_includes dkim_header.to_s, 's=brisbane'
|
66
|
+
assert_includes dkim_header.to_s, 'd=example.com'
|
67
|
+
assert_includes dkim_header.to_s, 'i=@example.com'
|
68
|
+
assert_includes dkim_header.to_s, 'c=relaxed/relaxed'
|
69
|
+
assert_includes dkim_header.to_s, 'q=dns/txt'
|
70
|
+
assert_includes dkim_header.to_s, 'bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8='
|
71
|
+
|
72
|
+
# TODO: double check signing of 'b' header
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_header_with_simple
|
76
|
+
Dkim.header_canonicalization = 'simple'
|
77
|
+
Dkim.body_canonicalization = 'simple'
|
78
|
+
Dkim.signing_algorithm = 'rsa-sha256'
|
79
|
+
Dkim.identity = '@example.com'
|
80
|
+
Interceptor.delivering_email(@mail)
|
81
|
+
dkim_header = @mail['DKIM-Signature']
|
82
|
+
assert dkim_header
|
83
|
+
assert_includes dkim_header.to_s, 'rsa-sha256'
|
84
|
+
assert_includes dkim_header.to_s, 's=brisbane'
|
85
|
+
assert_includes dkim_header.to_s, 'd=example.com'
|
86
|
+
assert_includes dkim_header.to_s, 'i=@example.com'
|
87
|
+
assert_includes dkim_header.to_s, 'c=simple/simple'
|
88
|
+
assert_includes dkim_header.to_s, 'q=dns/txt'
|
89
|
+
assert_includes dkim_header.to_s, 'bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8='
|
90
|
+
|
91
|
+
# TODO: double check signing of 'b' header
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_strips_exsting_headers
|
95
|
+
warnings = ""
|
96
|
+
klass = Class.new(Interceptor)
|
97
|
+
klass.class.send(:define_method, :warn) do |message|
|
98
|
+
warnings << message << "\n"
|
99
|
+
end
|
100
|
+
@mail = Mail.new(SIGNEDMAIL)
|
101
|
+
|
102
|
+
assert_equal 2, @mail.header.fields.count { |field| field.name =~ /^DKIM-Signature$/i }
|
103
|
+
assert_equal 2, @mail.encoded.scan('DKIM-Signature').count
|
104
|
+
|
105
|
+
klass.delivering_email(@mail)
|
106
|
+
|
107
|
+
# should give a warning
|
108
|
+
assert_includes warnings, 'Interceptor'
|
109
|
+
|
110
|
+
assert_equal 1, @mail.header.fields.count { |field| field.name =~ /^DKIM-Signature$/i }
|
111
|
+
assert_equal 1, @mail.encoded.scan('DKIM-Signature').count
|
112
|
+
end
|
113
|
+
|
114
|
+
def test_same_output_as_direct_usage
|
115
|
+
dkim_header = @mail['DKIM-Signature']
|
116
|
+
|
117
|
+
# Most necessary under simple
|
118
|
+
Dkim.header_canonicalization = 'simple'
|
119
|
+
Dkim.body_canonicalization = 'simple'
|
120
|
+
|
121
|
+
expected = Dkim.sign @mail.to_s
|
122
|
+
|
123
|
+
Interceptor.delivering_email(@mail)
|
124
|
+
|
125
|
+
assert_equal expected, @mail.to_s
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Dkim
|
4
|
+
class OptionsTest < Minitest::Test
|
5
|
+
def setup
|
6
|
+
klass = Class.new
|
7
|
+
klass.send :include, Options
|
8
|
+
@options = klass.new
|
9
|
+
end
|
10
|
+
def test_defaults_empty
|
11
|
+
assert_equal({}, @options.options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_all_options
|
15
|
+
@options.signing_algorithm = 'abc123'
|
16
|
+
assert_equal({:signing_algorithm => 'abc123'}, @options.options)
|
17
|
+
|
18
|
+
desired_options = {
|
19
|
+
:signing_algorithm => 'abc123',
|
20
|
+
:signable_headers => [],
|
21
|
+
:domain => 'example.net',
|
22
|
+
:identity => '@example.net',
|
23
|
+
:selector => 'selector',
|
24
|
+
:time => 'time',
|
25
|
+
:header_canonicalization => 'simple',
|
26
|
+
:body_canonicalization => 'simple'
|
27
|
+
}
|
28
|
+
|
29
|
+
desired_options.each do |key, value|
|
30
|
+
@options.send("#{key}=", value)
|
31
|
+
end
|
32
|
+
|
33
|
+
assert_equal(desired_options, @options.options)
|
34
|
+
|
35
|
+
desired_options.each do |key, value|
|
36
|
+
assert_equal value, @options.send("#{key}")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|