dkimverify 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,162 @@
1
+ require_relative 'exceptions'
2
+ require_relative 'parser'
3
+ require_relative 'malformed_key'
4
+
5
+ module DKIM
6
+ module Query
7
+ #
8
+ # Represents an individual DKIM signing key.
9
+ #
10
+ class Key
11
+
12
+ # DKIM version.
13
+ #
14
+ # @return [:DKIM1]
15
+ attr_reader :v
16
+ alias version v
17
+
18
+ # `g=` tag.
19
+ #
20
+ # @return [String, nil]
21
+ attr_reader :g
22
+ alias granularity g
23
+
24
+ # `h=` tag.
25
+ #
26
+ # @return [:sha1, :sha256, Array<:sha1, :sha256, String>, nil]
27
+ attr_reader :h
28
+ alias hash h
29
+
30
+ # `k=` tag.
31
+ #
32
+ # @return [:rsa, String]
33
+ attr_reader :k
34
+ alias key k
35
+
36
+ # `n=` tag.
37
+ #
38
+ # @return [String, nil]
39
+ attr_reader :n
40
+ alias notes n
41
+
42
+ # `p=` tag.
43
+ #
44
+ # @return [String, nil]
45
+ attr_reader :p
46
+ alias public_key p
47
+
48
+ # `s=` tag.
49
+ #
50
+ # @return [:email, :*, String, Array<:email, :*, String>, nil]
51
+ attr_reader :s
52
+ alias service_type s
53
+
54
+ # `t=` tag.
55
+ #
56
+ # @return [:y, :s, String, nil]
57
+ attr_reader :t
58
+ alias flags t
59
+
60
+ #
61
+ # Initialize the key.
62
+ #
63
+ # @param [Hash{Symbol => Symbol,String}] tags
64
+ # Tags for the key.
65
+ #
66
+ # @option tags [Symbol] :v
67
+ #
68
+ # @option tags [Symbol] :g
69
+ #
70
+ # @option tags [Symbol] :h
71
+ #
72
+ # @option tags [Symbol] :k
73
+ #
74
+ # @option tags [Symbol] :n
75
+ #
76
+ # @option tags [Symbol] :p
77
+ #
78
+ # @option tags [Symbol] :s
79
+ #
80
+ # @option tags [Symbol] :t
81
+ #
82
+ def initialize(tags={})
83
+ @v, @g, @h, @k, @n, @p, @s, @t = tags.values_at(:v,:g,:h,:k,:n,:p,:s,:t)
84
+ end
85
+
86
+ #
87
+ # Parses a DKIM Key record.
88
+ #
89
+ # @param [String] record
90
+ # The DKIM key record.
91
+ #
92
+ # @return [Key]
93
+ # The new key.
94
+ #
95
+ # @raise [InvalidKey]
96
+ # Could not parse the DKIM Key record.
97
+ #
98
+ def self.parse!(record)
99
+ new(Parser.parse(record))
100
+ rescue Parslet::ParseFailed => error
101
+ raise(InvalidKey.new(error.message,error.cause))
102
+ end
103
+
104
+ #
105
+ # Parses a DKIM Key record.
106
+ #
107
+ # @param [String] record
108
+ # The DKIM key record.
109
+ #
110
+ # @return [Key, MalformedKey]
111
+ # The parsed key. If the key could not be parsed, a {MalformedKey}
112
+ # will be returned.
113
+ #
114
+ def self.parse(record)
115
+ begin
116
+ parse!(record)
117
+ rescue Parslet::ParseFailed => error
118
+ MalformedKey.new(record,error.cause)
119
+ end
120
+ end
121
+
122
+ #
123
+ # Converts the key to a Hash.
124
+ #
125
+ # @return [Hash{:v,:g,:h,:k,:n,:p,:s,:t => Object}]
126
+ #
127
+ def to_hash
128
+ {
129
+ v: @v,
130
+ g: @g,
131
+ h: @h,
132
+ k: @k,
133
+ n: @n,
134
+ p: @p,
135
+ s: @s,
136
+ t: @t
137
+ }
138
+ end
139
+
140
+ #
141
+ # Converts the key back into a DKIM String.
142
+ #
143
+ # @return [String]
144
+ #
145
+ def to_s
146
+ tags = []
147
+
148
+ tags << "v=#{@v}" if @v
149
+ tags << "g=#{@g}" if @g
150
+ tags << "h=#{@h}" if @h
151
+ tags << "k=#{@k}" if @k
152
+ tags << "p=#{@p}" if @p
153
+ tags << "s=#{@s}" if @s
154
+ tags << "t=#{@t}" if @t
155
+ tags << "n=#{@n}" if @n
156
+
157
+ return tags.join('; ')
158
+ end
159
+
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,36 @@
1
+ module DKIM
2
+ module Query
3
+ #
4
+ # Represents a unparsable DKIM key.
5
+ #
6
+ class MalformedKey
7
+
8
+ # Raw value of the DKIM key.
9
+ #
10
+ # @return [String]
11
+ attr_reader :value
12
+
13
+ # Cause of the parser failure.
14
+ #
15
+ # @return [Parslet::Cause]
16
+ attr_reader :cause
17
+
18
+ #
19
+ # Initializes the malformed key.
20
+ #
21
+ # @param [String] value
22
+ # The raw DKIM key.
23
+ #
24
+ # @param [Parslet::Cause] cause
25
+ # The cause of the parser failure.
26
+ #
27
+ def initialize(value,cause)
28
+ @value = value
29
+ @cause = cause
30
+ end
31
+
32
+ alias to_s value
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,175 @@
1
+ require 'parslet'
2
+ require 'openssl'
3
+
4
+ module DKIM
5
+ module Query
6
+ #
7
+ # DKIM parser.
8
+ #
9
+ # @see https://tools.ietf.org/html/rfc6376#section-3
10
+ #
11
+ class Parser < Parslet::Parser
12
+
13
+ root :record
14
+ rule(:record) do
15
+ (
16
+ fws? >> key_tag >> fws? >>
17
+ (str(';') >> fws? >> key_tag >> fws?).repeat(0) >> str(';').maybe
18
+ ).as(:tag_list)
19
+ end
20
+
21
+ rule(:key_tag) do
22
+ (
23
+ key_v_tag |
24
+ key_g_tag |
25
+ key_h_tag |
26
+ key_k_tag |
27
+ key_n_tag |
28
+ key_p_tag |
29
+ key_s_tag |
30
+ key_t_tag
31
+ ).as(:tag)
32
+ end
33
+
34
+ private
35
+
36
+ def self.key_tag_rule(name,&block)
37
+ rule(:"key_#{name}_tag") do
38
+ str(name).as(:name) >>
39
+ fws? >> str('=') >> fws? >>
40
+ instance_eval(&block).as(:value)
41
+ end
42
+ end
43
+
44
+ def symbol(name)
45
+ str(name).as(:symbol)
46
+ end
47
+
48
+ public
49
+
50
+ key_tag_rule('v') { symbol('DKIM1') }
51
+ key_tag_rule('g') { key_g_tag_lpart }
52
+ rule(:key_g_tag_lpart) do
53
+ (
54
+ dot_atom_text.maybe >>
55
+ (str('*') >> dot_atom_text.maybe).maybe
56
+ ).as(:string)
57
+ end
58
+ rule(:dot_atom_text) do
59
+ atext.repeat(1) >> (str('.') >> atext.repeat(1)).repeat(0)
60
+ end
61
+
62
+ key_tag_rule('h') do
63
+ key_h_tag_alg >> (fws? >> str(':') >> fws? >> key_h_tag_alg).repeat(0)
64
+ end
65
+ rule(:key_h_tag_alg) { symbol('sha1') | symbol('sha256') | x_key_h_tag_alg }
66
+ rule(:x_key_h_tag_alg) { hyphenated_word.as(:string) }
67
+
68
+ key_tag_rule('k') { key_k_tag_type }
69
+ rule(:key_k_tag_type) { symbol('rsa') | x_key_k_tag_type }
70
+ rule(:x_key_k_tag_type) { hyphenated_word.as(:string) }
71
+
72
+ key_tag_rule('n') { qp_section.as(:string) }
73
+ key_tag_rule('p') { base64string.as(:asn1).maybe }
74
+ key_tag_rule('s') do
75
+ key_s_tag_type >> (fws? >> str(':') >> fws? >> key_s_tag_type).repeat(0)
76
+ end
77
+ rule(:key_s_tag_type) { symbol('email') | symbol('*') | x_key_s_tag_type }
78
+ rule(:x_key_s_tag_type) { hyphenated_word.as(:string) }
79
+
80
+ key_tag_rule('t') do
81
+ key_t_tag_flag >> (fws? >> str(':') >> fws? >> key_t_tag_flag).repeat(0)
82
+ end
83
+ rule(:key_t_tag_flag) { match['ys'].as(:symbol) | x_key_t_tag_flag }
84
+ rule(:x_key_t_tag_flag) { hyphenated_word.as(:string) }
85
+
86
+ #
87
+ # Section 2.6: DKIM-Quoted-Printable
88
+ #
89
+ rule(:dkim_quoted_printable) do
90
+ (fws | hex_octet | dkim_safe_char).repeat(0)
91
+ end
92
+ rule(:dkim_safe_char) do
93
+ match['\x21-\x3a'] | str("\x3c") | match['\x3e-\x7e']
94
+ end
95
+
96
+ #
97
+ # Section 2.4: Common ABNF Tokens
98
+ #
99
+ rule(:hyphenated_word) do
100
+ alpha >> (
101
+ (str('-').absnt? >> (alpha | digit)) |
102
+ (str('-').repeat(0) >> (alpha | digit))
103
+ ).repeat(0)
104
+ end
105
+ rule(:base64string) do
106
+ (alpha | digit | str('+') | str('/') | fws).repeat(1) >>
107
+ (str('=') >> fws? >> (str('=') >> fws?)).maybe
108
+ end
109
+
110
+ #
111
+ # Section 2.3: Whitespace
112
+ #
113
+ rule(:sp) { str(' ') }
114
+ rule(:crlf) { str("\r\n") }
115
+ rule(:wsp) { match['\t '] }
116
+ rule(:wsp?) { wsp.maybe }
117
+ rule(:lwsp) { (wsp | crlf >> wsp).repeat(0) }
118
+ rule(:fws) { (wsp.repeat(0) >> crlf).maybe >> wsp.repeat(1) }
119
+ rule(:fws?) { fws.maybe }
120
+
121
+ #
122
+ # Character rules
123
+ #
124
+ rule(:alpha) { match['a-zA-Z'] }
125
+ rule(:digit) { match['0-9'] }
126
+ rule(:alnum) { match['a-zA-Z0-9'] }
127
+ rule(:valchar) { match['\x21-\x3a'] | match['\x3c-\x7e'] }
128
+ rule(:alnumpunc) { match['a-zA-Z0-9_'] }
129
+ rule(:atext) { alnum | match['!#$%&\'*+\-/=?^ `{|}~'] }
130
+
131
+ #
132
+ # Quoted printable
133
+ #
134
+ rule(:qp_section) do
135
+ (wsp? >> ptext.repeat(1) >> (wsp >> ptext.repeat(1)).repeat(0)).maybe
136
+ end
137
+ rule(:ptext) { hex_octet | safe_char }
138
+ rule(:safe_char) { match['\x21-\x3c'] | match['\x3e-\x7e'] }
139
+ rule(:hex_octet) { str('=') >> match['0-9A-F'].repeat(2,2) }
140
+
141
+ class Transform < Parslet::Transform
142
+
143
+ rule(:symbol => simple(:name)) { name.to_sym }
144
+ rule(:string => simple(:text)) { text.to_s }
145
+ # XXX: temporarily disable ASN1 decoding, due to an OpenSSL bug.
146
+ # rule(:asn1 => simple(:blob)) { OpenSSL::ASN1.decode(blob) }
147
+ rule(:asn1 => simple(:blob)) { blob }
148
+
149
+ rule(tag: {name: simple(:name), value: subtree(:value)}) do
150
+ {name.to_sym => value}
151
+ end
152
+
153
+ rule(tag_list: subtree(:hashes)) do
154
+ case hashes
155
+ when Array then hashes.reduce(&:merge!)
156
+ else hashes
157
+ end
158
+ end
159
+
160
+ end
161
+
162
+ #
163
+ # Parses the text into structured data.
164
+ #
165
+ # @param [String] text
166
+ #
167
+ # @return [Hash]
168
+ #
169
+ def self.parse(text)
170
+ Transform.new.apply(new.parse(text))
171
+ end
172
+
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,74 @@
1
+ require 'resolv'
2
+
3
+ module DKIM
4
+ module Query
5
+ #
6
+ # Queries the domain for all DKIM selectors.
7
+ #
8
+ # @param [String] domain
9
+ # The domain to query.
10
+ #
11
+ # @option options [Array<String>] :selectors
12
+ # sub-domain selectors.
13
+ #
14
+ # @option options [Resolv::DNS] :resolver
15
+ # Optional resolver to use.
16
+ #
17
+ # @return [Hash{String => String}]
18
+ # The DKIM keys for the domain.
19
+ #
20
+ # @api semipublic
21
+ #
22
+ def self.query(domain,options={})
23
+ selectors = options.fetch(:selectors) { selectors_for(domain) }
24
+ resolver = options.fetch(:resolver) { Resolv::DNS.new }
25
+
26
+ keys = {}
27
+
28
+ selectors.each do |selector|
29
+ host = "#{selector}._domainkey.#{domain}"
30
+
31
+ begin
32
+ keys[selector] = resolver.getresource(
33
+ host, Resolv::DNS::Resource::IN::TXT
34
+ ).strings.join
35
+ rescue Resolv::ResolvError
36
+ end
37
+ end
38
+
39
+ return keys
40
+ end
41
+
42
+ # Default known selectors
43
+ SELECTORS = %w[default dkim google s1024 c1211 mandrill]
44
+
45
+ #
46
+ # DKIM query selectors for the host.
47
+ #
48
+ # @param [String] host
49
+ #
50
+ # @return [Array<String>]
51
+ #
52
+ # @api private
53
+ #
54
+ def self.selectors_for(host)
55
+ SELECTORS + [host_without_tld(host)]
56
+ end
57
+
58
+ #
59
+ # Removes the TLD from the hostname.
60
+ #
61
+ # @param [String] host
62
+ #
63
+ # @return [String]
64
+ #
65
+ # @api private
66
+ #
67
+ def self.host_without_tld(host)
68
+ if host.include?('.') then host[0,host.rindex('.')]
69
+ else host
70
+ end
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,6 @@
1
+ module DKIM
2
+ module Query
3
+ # dkim-query version
4
+ VERSION = '0.2.6'
5
+ end
6
+ end
@@ -0,0 +1,4 @@
1
+ require_relative 'query/version'
2
+ require_relative 'query/query'
3
+ require_relative 'query/key'
4
+ require_relative 'query/domain'
@@ -0,0 +1,96 @@
1
+ require 'spec_helper'
2
+ require 'dkim/query/domain'
3
+
4
+ describe Domain do
5
+ let(:domain) { 'yahoo.com' }
6
+ let(:key) do
7
+ Key.new(
8
+ k: :rsa,
9
+ p: "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDrEee0Ri4Juz+QfiWYui/E9UGSXau/2P8LjnTD8V4Unn+2FAZVGE3kL23bzeoULYv4PeleB3gfmJiDJOKU3Ns5L4KJAUUHjFwDebt0NP+sBK0VKeTATL2Yr/S3bT/xhy+1xtj4RkdV7fVxTn56Lb4udUnwuxK4V5b5PdOKj/+XcwIDAQAB",
10
+ n: "A 1024 bit key;",
11
+ )
12
+ end
13
+ let(:selector) { 's1024' }
14
+ let(:keys) { {selector => key} }
15
+
16
+ subject { described_class.new(domain,keys) }
17
+
18
+ describe "#initialize" do
19
+ it "should set name" do
20
+ expect(subject.name).to be domain
21
+ end
22
+
23
+ it "should set keys" do
24
+ expect(subject.keys).to be keys
25
+ end
26
+ end
27
+
28
+ describe ".parse" do
29
+ let(:key) do
30
+ %{k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDrEee0Ri4Juz+QfiWYui/E9UGSXau/2P8LjnTD8V4Unn+2FAZVGE3kL23bzeoULYv4PeleB3gfmJiDJOKU3Ns5L4KJAUUHjFwDebt0NP+sBK0VKeTATL2Yr/S3bT/xhy+1xtj4RkdV7fVxTn56Lb4udUnwuxK4V5b5PdOKj/+XcwIDAQAB; n=A 1024 bit key}
31
+ end
32
+ let(:keys) { {selector => key} }
33
+
34
+ subject { described_class.parse(domain,keys) }
35
+
36
+ it "should parse the keys" do
37
+ expect(subject.keys[selector]).to be_kind_of(Key)
38
+ end
39
+ end
40
+
41
+ describe ".query" do
42
+ subject { described_class.query(domain) }
43
+
44
+ it "should find all known keys" do
45
+ expect(subject.keys).to have_key('s1024')
46
+ end
47
+
48
+ context "with custom selectors" do
49
+ let(:selectors) { ['google', 's1024'] }
50
+
51
+ subject { described_class.query(domain, selectors: selectors) }
52
+
53
+ it "should query those selectors only" do
54
+ expect(subject.keys).to have_key('s1024')
55
+ end
56
+ end
57
+
58
+ context "with no selectors" do
59
+ let(:selectors) { [] }
60
+
61
+ subject { described_class.query(domain, selectors: selectors) }
62
+
63
+ it "should not find any keys" do
64
+ expect(subject.keys).to be_empty
65
+ end
66
+ end
67
+ end
68
+
69
+ describe "#each" do
70
+ context "when given a block" do
71
+ it "should yield each Key" do
72
+ expect { |b| subject.each(&b) }.to yield_with_args(key)
73
+ end
74
+ end
75
+
76
+ context "when not given a block" do
77
+ it "should return an Enumerator" do
78
+ expect(subject.each).to be_kind_of(Enumerator)
79
+ end
80
+ end
81
+ end
82
+
83
+ describe "#[]" do
84
+ context "when given a valid selector" do
85
+ it "should return the key" do
86
+ expect(subject[selector]).to be key
87
+ end
88
+ end
89
+
90
+ context "when given an unknown selector" do
91
+ it "should return nil" do
92
+ expect(subject['foo']).to be_nil
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,117 @@
1
+ require 'spec_helper'
2
+ require 'dkim/query/key'
3
+
4
+ describe Key do
5
+ let(:k) { :rsa }
6
+ let(:p) { "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDrEee0Ri4Juz+QfiWYui/E9UGSXau/2P8LjnTD8V4Unn+2FAZVGE3kL23bzeoULYv4PeleB3gfmJiDJOKU3Ns5L4KJAUUHjFwDebt0NP+sBK0VKeTATL2Yr/S3bT/xhy+1xtj4RkdV7fVxTn56Lb4udUnwuxK4V5b5PdOKj/+XcwIDAQAB" }
7
+ let(:t) { :s }
8
+ let(:n) { "A 1024 bit key;" }
9
+
10
+ subject do
11
+ described_class.new(
12
+ k: k,
13
+ p: p,
14
+ t: t,
15
+ n: n
16
+ )
17
+ end
18
+
19
+ describe "#initialize" do
20
+ it "should set @k" do
21
+ expect(subject.k).to be k
22
+ end
23
+
24
+ it "should set @p" do
25
+ expect(subject.p).to be p
26
+ end
27
+
28
+ it "should set @t" do
29
+ expect(subject.t).to be t
30
+ end
31
+
32
+ it "should set @n" do
33
+ expect(subject.n).to be n
34
+ end
35
+ end
36
+
37
+ let(:record) { %{k=#{k}; p=#{p}; t=#{t}; n=#{n}} }
38
+ let(:invalid_record) { "v=spf1" }
39
+
40
+ describe ".parse!" do
41
+ context "when parsing a valid DKIM Key record" do
42
+ subject { described_class.parse!(record) }
43
+
44
+ it "should return a Key" do
45
+ expect(subject).to be_kind_of(described_class)
46
+ end
47
+
48
+ it "should parse k" do
49
+ expect(subject.k).to be k
50
+ end
51
+
52
+ it "should parse p" do
53
+ expect(subject.p).to be == p
54
+ end
55
+
56
+ it "should parse t" do
57
+ expect(subject.t).to be t
58
+ end
59
+
60
+ it "should parse n" do
61
+ expect(subject.n).to be == n
62
+ end
63
+ end
64
+
65
+ context "when parsing an invalid DKIM Key record" do
66
+ it "should raise an InvalidKey exception" do
67
+ expect {
68
+ described_class.parse!(invalid_record)
69
+ }.to raise_error(InvalidKey)
70
+ end
71
+ end
72
+ end
73
+
74
+ describe ".parse" do
75
+ context "when parsing a valid DKIM Key record" do
76
+ subject { described_class.parse(record) }
77
+
78
+ it "should return a Key" do
79
+ expect(subject).to be_kind_of(described_class)
80
+ end
81
+ end
82
+
83
+ context "when parsing an invalid DKIM Key record" do
84
+ subject { described_class.parse(invalid_record) }
85
+
86
+ it "should return a MalformedKey" do
87
+ expect(subject).to be_kind_of(MalformedKey)
88
+ end
89
+ end
90
+ end
91
+
92
+ describe "#to_hash" do
93
+ subject { super().to_hash }
94
+
95
+ it "should include :k" do
96
+ expect(subject[:k]).to be == k
97
+ end
98
+
99
+ it "should include :p" do
100
+ expect(subject[:p]).to be == p
101
+ end
102
+
103
+
104
+ it "should include :t" do
105
+ expect(subject[:t]).to be == t
106
+ end
107
+ it "should include :n" do
108
+ expect(subject[:n]).to be == n
109
+ end
110
+ end
111
+
112
+ describe "#to_s" do
113
+ it "should return a semicolon deliminited string" do
114
+ expect(subject.to_s).to be == "k=#{k}; p=#{p}; t=#{t}; n=#{n}"
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+ require 'dkim/query/malformed_key'
3
+
4
+ describe MalformedKey do
5
+ describe "#to_s" do
6
+ let(:value) { "foo bar" }
7
+ let(:cause) { double(:parslet_error) }
8
+
9
+ subject { described_class.new(value,cause) }
10
+
11
+ it "should return the value" do
12
+ expect(subject.to_s).to be == value
13
+ end
14
+ end
15
+ end