dkimverify 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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