dkimverify 0.0.6 → 0.0.7

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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/dkimverify.gemspec +1 -1
  3. data/dkimverify.rb +273 -272
  4. metadata +3 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0a30e99530c7485db52e9583ad67b4565b22eadd
4
- data.tar.gz: 0876d9ad6008badfc29bf3c05057b2894af84771
3
+ metadata.gz: 72a972bced6eedcae611a115ee46c5aceb7f3230
4
+ data.tar.gz: 88672a73ab1f4a176d845246afa082d31e15b607
5
5
  SHA512:
6
- metadata.gz: a4334b8fb2321ca976c25911447d1f95e3fd2fd7752640f93aa0c19f6622dde2c6d6da27e00ef75cdc3a9089e159d8f1df3d94e6eb4f8171b4333475d28aa574
7
- data.tar.gz: 1819a0bef43d2aa4c95e2f69f4c3ec2b037d9c7a85eb6efe14b0bcbda6ab9257f8fcc2b5e0391df41053ef10e84e8a856edb21631ac947bc8f1a2d471cf95ab3
6
+ metadata.gz: 015d9d955eb0a8ac8c03146e7ee1eace2858d665a3174597d26af7ada3958f97ab7739114c37cea74cd311ec75acc2255d5d5b5e62731a782ec465f034b00d53
7
+ data.tar.gz: 8898575eb90534f1404f1148133efe6297440cdd7c152d2060c9491c369ea0d9dc11bbfc8671b6879a9d996d8a37a64bcfa38a0b9b005661733fbab10803b3dc
data/dkimverify.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |gem|
4
4
  gem.name = "dkimverify"
5
- gem.version = '0.0.6'
5
+ gem.version = '0.0.7'
6
6
  gem.authors = ["Jeremy B. Merrill"]
7
7
  gem.license = "MIT"
8
8
  gem.email = ["jeremybmerrill@gmail.com"]
data/dkimverify.rb CHANGED
@@ -6,331 +6,332 @@ require 'resolv'
6
6
  # TODO make this an option somehow
7
7
  $debuglog = nil #STDERR # nil # alternatively, set this to `STDERR` to log to stdout.
8
8
 
9
- module Mail
10
- class MessageFormatError < StandardError; end
9
+ module DkimVerify
10
+ module Mail
11
+ class MessageFormatError < StandardError; end
11
12
 
12
- class HeaderHash < Hash
13
- def get(header_name)
14
- self[get_name(header_name)]
15
- end
13
+ class HeaderHash < Hash
14
+ def get(header_name)
15
+ self[get_name(header_name)]
16
+ end
16
17
 
17
- def get_name(header_name)
18
- keys.find{|k| k.downcase == header_name.downcase }
18
+ def get_name(header_name)
19
+ keys.find{|k| k.downcase == header_name.downcase }
20
+ end
19
21
  end
20
- end
21
22
 
22
- class Message
23
- def initialize(msg)
24
- @raw_message = msg
25
- @raw_headers = []
26
- @body = nil
27
- @parsed = false
28
- end
23
+ class Message
24
+ def initialize(msg)
25
+ @raw_message = msg
26
+ @raw_headers = []
27
+ @body = nil
28
+ @parsed = false
29
+ end
29
30
 
30
- def headers
31
- self.parse! unless @parsed
32
- @headers
33
- end
31
+ def headers
32
+ self.parse! unless @parsed
33
+ @headers
34
+ end
34
35
 
35
- def body
36
- self.parse! unless @parsed
37
- @body
38
- end
36
+ def body
37
+ self.parse! unless @parsed
38
+ @body
39
+ end
39
40
 
40
- def parse!
41
- """Parse a message in RFC822 format.
41
+ def parse!
42
+ """Parse a message in RFC822 format.
42
43
 
43
- @param message: The message in RFC822 format. Either CRLF or LF is an accepted line separator.
44
+ @param message: The message in RFC822 format. Either CRLF or LF is an accepted line separator.
44
45
 
45
- @return Returns a tuple of (headers, body) where headers is a list of (name, value) pairs.
46
- The body is a CRLF-separated string.
46
+ @return Returns a tuple of (headers, body) where headers is a list of (name, value) pairs.
47
+ The body is a CRLF-separated string.
47
48
 
48
- """
49
+ """
49
50
 
50
- lines = @raw_message.split(/\r?\n/)
51
- i = 0
52
- while i < lines.size
53
- if lines[i].size == 0
54
- # End of headers, return what we have plus the body, excluding the blank line.
55
- i += 1
56
- break
57
- end
58
- if /[\x09\x20]/.match lines[i][0]
59
- @raw_headers[-1][1] += lines[i]+"\r\n"
60
- else
61
- m = /([\x21-\x7e]+?):/.match lines[i]
62
- if m
63
- @raw_headers << [m[1], lines[i][m.end(0)..-1]+"\r\n"]
64
- elsif lines[i].start_with?("From ")
65
-
51
+ lines = @raw_message.split(/\r?\n/)
52
+ i = 0
53
+ while i < lines.size
54
+ if lines[i].size == 0
55
+ # End of headers, return what we have plus the body, excluding the blank line.
56
+ i += 1
57
+ break
58
+ end
59
+ if /[\x09\x20]/.match lines[i][0]
60
+ @raw_headers[-1][1] += lines[i]+"\r\n"
66
61
  else
67
- raise MessageFormatError.new("Unexpected characters in RFC822 header: #{lines[i]}")
62
+ m = /([\x21-\x7e]+?):/.match lines[i]
63
+ if m
64
+ @raw_headers << [m[1], lines[i][m.end(0)..-1]+"\r\n"]
65
+ elsif lines[i].start_with?("From ")
66
+
67
+ else
68
+ raise MessageFormatError.new("Unexpected characters in RFC822 header: #{lines[i]}")
69
+ end
68
70
  end
71
+ i += 1
69
72
  end
70
- i += 1
73
+ @body = lines[i..-1].join("\r\n") + "\r\n"
74
+ @headers = HeaderHash[*@raw_headers.reverse.flatten(1)]
71
75
  end
72
- @body = lines[i..-1].join("\r\n") + "\r\n"
73
- @headers = HeaderHash[*@raw_headers.reverse.flatten(1)]
74
76
  end
75
77
  end
76
- end
77
78
 
78
- module Dkim
79
- # what are these magic numbers?!
80
- # These values come from RFC 3447, section 9.2 Notes, page 43.
81
- # cf. https://github.com/emboss/krypt/blob/c804f736d4dbaa4425014d036d2e68d8ee66d559/lib/krypt/asn1/common.rb
82
- # SHA1 = algorithm_null_params('1.3.14.3.2.26')
83
- # SHA256 = algorithm_null_params('2.16.840.1.101.3.4.2.1')
84
- OpenSSL::ASN1::ObjectId.register('1.3.14.3.2.26', 'sha1', 'HASHID_SHA1')
85
- OpenSSL::ASN1::ObjectId.register('2.16.840.1.101.3.4.2.1', 'sha256', 'HASHID_SHA256')
86
- HASHID_SHA1 = OpenSSL::ASN1::ObjectId.new('sha1')
87
- HASHID_SHA256 = OpenSSL::ASN1::ObjectId.new('sha256')
88
-
89
- class DkimError < StandardError; end
90
- class DkimTempFail < DkimError; end
91
- class DkimPermFail < DkimError; end
92
- class InvalidDkimSignature < DkimPermFail; end
93
- class DkimVerificationFailure < DkimPermFail; end
94
-
95
- #TODO: what is this kind of key-value string even called?
96
- def self.parse_header_kv(input_str)
97
- parsed = {}
98
- input_str.split(/\s*;\s*/m).each do |key_val|
99
- if m = key_val.match(/(\w+)\s*=\s*(.*)/m)
100
- parsed[m[1]] = m[2]
79
+ module Verification
80
+ # what are these magic numbers?!
81
+ # These values come from RFC 3447, section 9.2 Notes, page 43.
82
+ # cf. https://github.com/emboss/krypt/blob/c804f736d4dbaa4425014d036d2e68d8ee66d559/lib/krypt/asn1/common.rb
83
+ # SHA1 = algorithm_null_params('1.3.14.3.2.26')
84
+ # SHA256 = algorithm_null_params('2.16.840.1.101.3.4.2.1')
85
+ OpenSSL::ASN1::ObjectId.register('1.3.14.3.2.26', 'sha1', 'HASHID_SHA1')
86
+ OpenSSL::ASN1::ObjectId.register('2.16.840.1.101.3.4.2.1', 'sha256', 'HASHID_SHA256')
87
+ HASHID_SHA1 = OpenSSL::ASN1::ObjectId.new('sha1')
88
+ HASHID_SHA256 = OpenSSL::ASN1::ObjectId.new('sha256')
89
+
90
+ class DkimError < StandardError; end
91
+ class DkimTempFail < DkimError; end
92
+ class DkimPermFail < DkimError; end
93
+ class InvalidDkimSignature < DkimPermFail; end
94
+ class DkimVerificationFailure < DkimPermFail; end
95
+
96
+ #TODO: what is this kind of key-value string even called?
97
+ def self.parse_header_kv(input_str)
98
+ parsed = {}
99
+ input_str.split(/\s*;\s*/m).each do |key_val|
100
+ if m = key_val.match(/(\w+)\s*=\s*(.*)/m)
101
+ parsed[m[1]] = m[2]
102
+ end
101
103
  end
104
+ parsed
102
105
  end
103
- parsed
104
- end
105
-
106
- class Verifier
107
- def initialize(email_stringy_thing)
108
- mail = Mail::Message.new(email_stringy_thing)
109
- @headers = mail.headers
110
- @body = mail.body
111
- end
112
-
113
-
114
- def verify!
115
- return false if @headers.get("DKIM-Signature").nil?
116
-
117
- dkim_signature_str = @headers.get("DKIM-Signature").to_s
118
- @dkim_signature = Dkim.parse_header_kv(dkim_signature_str)
119
- validate_signature! # just checking to make sure we have all the ingredients we need to actually verify the signature
120
106
 
121
- figure_out_canonicalization_methods!
122
- verify_body_hash!
107
+ class Verifier
108
+ def initialize(email_stringy_thing)
109
+ mail = Mail::Message.new(email_stringy_thing)
110
+ @headers = mail.headers
111
+ @body = mail.body
112
+ end
123
113
 
124
- # 'b=' is the signed message headers' hash.
125
- # we need to decrypt the 'b=' value (with the public key)
126
- # and compare it with the computed headers_hash.
127
- # decrypted_header_hash is the "expected" value.
128
- my_headers_hash = headers_hash
129
- my_decrypted_header_hash = decrypted_header_hash
130
114
 
131
- raise DkimVerificationFailure.new("header hash signatures sizes don't match") if my_decrypted_header_hash.size != my_headers_hash.size
132
-
133
- # Byte-by-byte compare of signatures
134
- does_signature_match = my_decrypted_header_hash.bytes.zip(my_headers_hash.bytes).all?{|exp, got| exp == got }
135
- raise DkimVerificationFailure.new("header hash signatures don't match. expected #{my_decrypted_header_hash}, got #{my_headers_hash}") unless does_signature_match
136
- return does_signature_match # always true, but this is a good guarantee of somebody accidentally refactoring this to always return true
137
- end
115
+ def verify!
116
+ return false if @headers.get("DKIM-Signature").nil?
138
117
 
139
- private
118
+ dkim_signature_str = @headers.get("DKIM-Signature").to_s
119
+ @dkim_signature = Verification.parse_header_kv(dkim_signature_str)
120
+ validate_signature! # just checking to make sure we have all the ingredients we need to actually verify the signature
140
121
 
122
+ figure_out_canonicalization_methods!
123
+ verify_body_hash!
141
124
 
142
- def verify_body_hash!
143
- # here we're figuring out what algorithm to use for computing the signature
144
- hasher, @hashid = if @dkim_signature['a'] == "rsa-sha1"
145
- [Digest::SHA1, HASHID_SHA1]
146
- elsif @dkim_signature['a'] == "rsa-sha256"
147
- [Digest::SHA256, HASHID_SHA256]
148
- else
149
- $debuglog.puts "couldn't figure out the right algorithm to use"
150
- exit 1
151
- end
152
-
153
- body = Dkim.canonicalize_body(@body, @how_to_canonicalize_body)
154
-
155
-
156
- bodyhash = hasher.digest(body)
125
+ # 'b=' is the signed message headers' hash.
126
+ # we need to decrypt the 'b=' value (with the public key)
127
+ # and compare it with the computed headers_hash.
128
+ # decrypted_header_hash is the "expected" value.
129
+ my_headers_hash = headers_hash
130
+ my_decrypted_header_hash = decrypted_header_hash
157
131
 
158
- $debuglog.puts "bh: #{Base64.encode64(bodyhash)}" unless $debuglog.nil?
132
+ raise DkimVerificationFailure.new("header hash signatures sizes don't match") if my_decrypted_header_hash.size != my_headers_hash.size
133
+
134
+ # Byte-by-byte compare of signatures
135
+ does_signature_match = my_decrypted_header_hash.bytes.zip(my_headers_hash.bytes).all?{|exp, got| exp == got }
136
+ raise DkimVerificationFailure.new("header hash signatures don't match. expected #{my_decrypted_header_hash}, got #{my_headers_hash}") unless does_signature_match
137
+ return does_signature_match # always true, but this is a good guarantee of somebody accidentally refactoring this to always return true
138
+ end
159
139
 
160
- if bodyhash != Base64.decode64(@dkim_signature['bh'].gsub(/\s+/, ''))
161
- error_msg = "body hash mismatch (got #{Base64.encode64(bodyhash)}, expected #{@dkim_signature['bh']})"
162
- $debuglog.puts error_msg unless $debuglog.nil?
163
- raise DkimVerificationFailure.new(error_msg)
140
+ private
141
+
142
+
143
+ def verify_body_hash!
144
+ # here we're figuring out what algorithm to use for computing the signature
145
+ hasher, @hashid = if @dkim_signature['a'] == "rsa-sha1"
146
+ [Digest::SHA1, HASHID_SHA1]
147
+ elsif @dkim_signature['a'] == "rsa-sha256"
148
+ [Digest::SHA256, HASHID_SHA256]
149
+ else
150
+ $debuglog.puts "couldn't figure out the right algorithm to use"
151
+ exit 1
152
+ end
153
+
154
+ body = Verification.canonicalize_body(@body, @how_to_canonicalize_body)
155
+
156
+
157
+ bodyhash = hasher.digest(body)
158
+
159
+ $debuglog.puts "bh: #{Base64.encode64(bodyhash)}" unless $debuglog.nil?
160
+
161
+ if bodyhash != Base64.decode64(@dkim_signature['bh'].gsub(/\s+/, ''))
162
+ error_msg = "body hash mismatch (got #{Base64.encode64(bodyhash)}, expected #{@dkim_signature['bh']})"
163
+ $debuglog.puts error_msg unless $debuglog.nil?
164
+ raise DkimVerificationFailure.new(error_msg)
165
+ end
166
+ nil
164
167
  end
165
- nil
166
- end
167
168
 
168
169
 
169
- # here we're figuring out the canonicalization algorithm for the body and for the headers
170
- def figure_out_canonicalization_methods!
171
- c_match = @dkim_signature['c'].match(/(\w+)(?:\/(\w+))?$/)
172
- if not c_match
173
- $debuglog.puts "can't figure out canonicalization ('c=')"
174
- return false
170
+ # here we're figuring out the canonicalization algorithm for the body and for the headers
171
+ def figure_out_canonicalization_methods!
172
+ c_match = @dkim_signature['c'].match(/(\w+)(?:\/(\w+))?$/)
173
+ if not c_match
174
+ $debuglog.puts "can't figure out canonicalization ('c=')"
175
+ return false
176
+ end
177
+ @how_to_canonicalize_headers = c_match[1]
178
+ if c_match[2]
179
+ @how_to_canonicalize_body = c_match[2]
180
+ else
181
+ @how_to_canonicalize_body = "simple"
182
+ end
183
+ raise ArgumentError, "invalid canonicalization method for headers" unless ["relaxed", "simple"].include?(@how_to_canonicalize_headers)
184
+ raise ArgumentError, "invalid canonicalization method for body" unless ["relaxed", "simple"].include?(@how_to_canonicalize_body)
175
185
  end
176
- @how_to_canonicalize_headers = c_match[1]
177
- if c_match[2]
178
- @how_to_canonicalize_body = c_match[2]
179
- else
180
- @how_to_canonicalize_body = "simple"
186
+
187
+ def public_key
188
+ # here we're getting the website's actual public key from the DNS system
189
+ # s = dnstxt(sig['s']+"._domainkey."+sig['d']+".")
190
+ # dkim_record_from_dns = DKIM::Query::Domain.query(@dkim_signature['d'], {:selectors => [@dkim_signature['s']]}).keys[@dkim_signature['s']]
191
+ txt = Resolv::DNS.open{|dns| dns.getresources("#{@dkim_signature['s']}._domainkey.#{@dkim_signature['d']}", Resolv::DNS::Resource::IN::TXT).map(&:data) }
192
+ raise DkimTempFail.new("couldn't get public key from DNS system for #{@dkim_signature['s']}/#{@dkim_signature['d']}") if txt.first.nil?
193
+ parsed_txt = Verification.parse_header_kv(txt.first)
194
+ raise DkimTempFail.new("couldn't get public key from DNS system for #{@dkim_signature['s']}/#{@dkim_signature['d']}") if !parsed_txt.keys.include?("p")
195
+ publickey_asn1 = OpenSSL::ASN1.decode(Base64.decode64(parsed_txt["p"]))
196
+ publickey = publickey_asn1.value[1].value
181
197
  end
182
- raise ArgumentError, "invalid canonicalization method for headers" unless ["relaxed", "simple"].include?(@how_to_canonicalize_headers)
183
- raise ArgumentError, "invalid canonicalization method for body" unless ["relaxed", "simple"].include?(@how_to_canonicalize_body)
184
- end
185
198
 
186
- def public_key
187
- # here we're getting the website's actual public key from the DNS system
188
- # s = dnstxt(sig['s']+"._domainkey."+sig['d']+".")
189
- # dkim_record_from_dns = DKIM::Query::Domain.query(@dkim_signature['d'], {:selectors => [@dkim_signature['s']]}).keys[@dkim_signature['s']]
190
- txt = Resolv::DNS.open{|dns| dns.getresources("#{@dkim_signature['s']}._domainkey.#{@dkim_signature['d']}", Resolv::DNS::Resource::IN::TXT).map(&:data) }
191
- raise DkimTempFail.new("couldn't get public key from DNS system for #{@dkim_signature['s']}/#{@dkim_signature['d']}") if txt.first.nil?
192
- parsed_txt = Dkim.parse_header_kv(txt.first)
193
- raise DkimTempFail.new("couldn't get public key from DNS system for #{@dkim_signature['s']}/#{@dkim_signature['d']}") if !parsed_txt.keys.include?("p")
194
- publickey_asn1 = OpenSSL::ASN1.decode(Base64.decode64(parsed_txt["p"]))
195
- publickey = publickey_asn1.value[1].value
196
- end
199
+ def headers_to_sign
200
+
201
+ # we figure out which headers we care about, then canonicalize them
202
+ header_fields_to_include = @dkim_signature['h'].split(/\s*:\s*/)
203
+ $debuglog.puts "header_fields_to_include: #{header_fields_to_include}" unless $debuglog.nil?
204
+ canonicalized_headers = []
205
+ header_fields_to_include_with_values = header_fields_to_include.map do |header_name|
206
+ header_val = (hstr = @headers.get(header_name)).nil? ? '' : hstr #.split(":")[1..-1].join(":")
207
+ [header_name, header_val ]
208
+ end
209
+ canonicalized_headers = Verification.canonicalize_headers(header_fields_to_include_with_values, @how_to_canonicalize_headers)
197
210
 
198
- def headers_to_sign
211
+ canonicalized_headers += Verification.canonicalize_headers([
212
+ [
213
+ @headers.get_name("DKIM-Signature").to_s,
214
+ @headers.get("DKIM-Signature").to_s.split(@dkim_signature['b']).join('')
215
+ ]
216
+ ], @how_to_canonicalize_headers).map{|x| [x[0], x[1].rstrip()] }
199
217
 
200
- # we figure out which headers we care about, then canonicalize them
201
- header_fields_to_include = @dkim_signature['h'].split(/\s*:\s*/)
202
- $debuglog.puts "header_fields_to_include: #{header_fields_to_include}" unless $debuglog.nil?
203
- canonicalized_headers = []
204
- header_fields_to_include_with_values = header_fields_to_include.map do |header_name|
205
- header_val = (hstr = @headers.get(header_name)).nil? ? '' : hstr #.split(":")[1..-1].join(":")
206
- [header_name, header_val ]
218
+ $debuglog.puts "verify headers: #{canonicalized_headers}" unless $debuglog.nil?
219
+ canonicalized_headers
207
220
  end
208
- canonicalized_headers = Dkim.canonicalize_headers(header_fields_to_include_with_values, @how_to_canonicalize_headers)
209
221
 
210
- canonicalized_headers += Dkim.canonicalize_headers([
211
- [
212
- @headers.get_name("DKIM-Signature").to_s,
213
- @headers.get("DKIM-Signature").to_s.split(@dkim_signature['b']).join('')
214
- ]
215
- ], @how_to_canonicalize_headers).map{|x| [x[0], x[1].rstrip()] }
222
+ def headers_digest
223
+ hasher = if @dkim_signature['a'] == "rsa-sha1"
224
+ Digest::SHA1
225
+ elsif @dkim_signature['a'] == "rsa-sha256"
226
+ Digest::SHA256
227
+ else
228
+ raise InvalidDkimSignature.new "couldn't figure out the right algorithm to use"
229
+ end.new
230
+ headers_to_sign.each do |header|
231
+ hasher.update(header[0])
232
+ hasher.update(":")
233
+ hasher.update(header[1])
234
+ end
235
+ digest = hasher.digest
236
+ $debuglog.puts "verify digest: #{ digest.each_byte.map { |b| b.to_s(16) }.join ' ' }" unless $debuglog.nil?
237
+ digest
238
+ end
239
+
216
240
 
217
- $debuglog.puts "verify headers: #{canonicalized_headers}" unless $debuglog.nil?
218
- canonicalized_headers
219
- end
241
+ def headers_hash
242
+ dinfo = OpenSSL::ASN1::Sequence.new([
243
+ OpenSSL::ASN1::Sequence.new([
244
+ @hashid,
245
+ OpenSSL::ASN1::Null.new(nil),
246
+ ]),
247
+ OpenSSL::ASN1::OctetString.new(headers_digest),
248
+ ])
249
+ headers_der = Base64.encode64(dinfo.to_der).gsub(/\s+/, '')
250
+ $debuglog.puts "headers_hash: #{headers_der}" unless $debuglog.nil?
251
+ headers_der
252
+ end
220
253
 
221
- def headers_digest
222
- hasher = if @dkim_signature['a'] == "rsa-sha1"
223
- Digest::SHA1
224
- elsif @dkim_signature['a'] == "rsa-sha256"
225
- Digest::SHA256
226
- else
227
- raise InvalidDkimSignature.new "couldn't figure out the right algorithm to use"
228
- end.new
229
- headers_to_sign.each do |header|
230
- hasher.update(header[0])
231
- hasher.update(":")
232
- hasher.update(header[1])
254
+ def decrypted_header_hash
255
+ begin
256
+ decrypted_header_hash_bytes = OpenSSL::PKey::RSA.new(public_key).public_decrypt(Base64.decode64(@dkim_signature['b']))
257
+ rescue OpenSSL::PKey::RSAError
258
+ raise DkimPermFail.new "couldn't decrypt header hash with public key"
259
+ end
260
+ ret = Base64.encode64(decrypted_header_hash_bytes).gsub(/\s+/, '')
261
+ $debuglog.puts "decrypted_header_hash: #{ret}" unless $debuglog.nil?
262
+ ret
233
263
  end
234
- digest = hasher.digest
235
- $debuglog.puts "verify digest: #{ digest.each_byte.map { |b| b.to_s(16) }.join ' ' }" unless $debuglog.nil?
236
- digest
237
- end
238
-
239
-
240
- def headers_hash
241
- dinfo = OpenSSL::ASN1::Sequence.new([
242
- OpenSSL::ASN1::Sequence.new([
243
- @hashid,
244
- OpenSSL::ASN1::Null.new(nil),
245
- ]),
246
- OpenSSL::ASN1::OctetString.new(headers_digest),
247
- ])
248
- headers_der = Base64.encode64(dinfo.to_der).gsub(/\s+/, '')
249
- $debuglog.puts "headers_hash: #{headers_der}" unless $debuglog.nil?
250
- headers_der
251
- end
252
264
 
253
- def decrypted_header_hash
254
- begin
255
- decrypted_header_hash_bytes = OpenSSL::PKey::RSA.new(public_key).public_decrypt(Base64.decode64(@dkim_signature['b']))
256
- rescue OpenSSL::PKey::RSAError
257
- raise DkimPermFail.new "couldn't decrypt header hash with public key"
265
+ def validate_signature!
266
+ # version: only version 1 is defined
267
+ raise InvalidDkimSignature.new("DKIM signature is missing required tag v=") unless @dkim_signature.include?('v')
268
+ raise InvalidDkimSignature.new("DKIM signature v= value is invalid (got \"#{@dkim_signature['v']}\"; expected \"1\")") unless @dkim_signature['v'] == "1"
269
+
270
+ # encryption algorithm
271
+ raise InvalidDkimSignature.new("DKIM signature is missing required tag a=") unless @dkim_signature.include?('a')
272
+
273
+ # header hash
274
+ raise InvalidDkimSignature.new("DKIM signature is missing required tag b=") unless @dkim_signature.include?('b')
275
+ raise InvalidDkimSignature.new("DKIM signature b= value is not valid base64") unless @dkim_signature['b'].match(/[\s0-9A-Za-z+\/]+=*$/)
276
+ raise InvalidDkimSignature.new("DKIM signature is missing required tag h=") unless @dkim_signature.include?('h')
277
+
278
+ # body hash (not directly encrypted)
279
+ raise InvalidDkimSignature.new("DKIM signature is missing required tag bh=") unless @dkim_signature.include?('bh')
280
+ raise InvalidDkimSignature.new("DKIM signature bh= value is not valid base64") unless @dkim_signature['bh'].match(/[\s0-9A-Za-z+\/]+=*$/)
281
+
282
+ # domain selector
283
+ raise InvalidDkimSignature.new("DKIM signature is missing required tag d=") unless @dkim_signature.include?('d')
284
+ raise InvalidDkimSignature.new("DKIM signature is missing required tag s=") unless @dkim_signature.include?('s')
285
+
286
+ # these are expiration dates, which are not checked above.
287
+ raise InvalidDkimSignature.new("DKIM signature t= value is not a valid decimal integer") unless @dkim_signature['t'].nil? || @dkim_signature['t'].match(/\d+$/)
288
+ raise InvalidDkimSignature.new("DKIM signature x= value is not a valid decimal integer") unless @dkim_signature['x'].nil? || @dkim_signature['x'].match(/\d+$/)
289
+ raise InvalidDkimSignature.new("DKIM signature x= value is less than t= (and must be greater than or equal to t=). (x=#{@dkim_signature['x']}, t=#{@dkim_signature['t']}) ") unless @dkim_signature['x'].nil? || @dkim_signature['x'].to_i >= @dkim_signature['t'].to_i
290
+
291
+ # other unimplemented stuff
292
+ raise InvalidDkimSignature.new("DKIM signature i= domain is not a subdomain of d= (i=#{@dkim_signature[i]} d=#{@dkim_signature[d]})") if @dkim_signature['i'] && !(@dkim_signature['i'].end_with?(@dkim_signature['d']) || ["@", ".", "@."].include?(@dkim_signature['i'][-@dkim_signature['d'].size-1]))
293
+ raise InvalidDkimSignature.new("DKIM signature l= value is invalid") if @dkim_signature['l'] && !@dkim_signature['l'].match(/\d{,76}$/)
294
+ raise InvalidDkimSignature.new("DKIM signature q= value is invalid (got \"#{@dkim_signature['q']}\"; expected \"dns/txt\")") if @dkim_signature['q'] && @dkim_signature['q'] != "dns/txt"
258
295
  end
259
- ret = Base64.encode64(decrypted_header_hash_bytes).gsub(/\s+/, '')
260
- $debuglog.puts "decrypted_header_hash: #{ret}" unless $debuglog.nil?
261
- ret
262
296
  end
263
297
 
264
- def validate_signature!
265
- # version: only version 1 is defined
266
- raise InvalidDkimSignature.new("DKIM signature is missing required tag v=") unless @dkim_signature.include?('v')
267
- raise InvalidDkimSignature.new("DKIM signature v= value is invalid (got \"#{@dkim_signature['v']}\"; expected \"1\")") unless @dkim_signature['v'] == "1"
268
-
269
- # encryption algorithm
270
- raise InvalidDkimSignature.new("DKIM signature is missing required tag a=") unless @dkim_signature.include?('a')
271
-
272
- # header hash
273
- raise InvalidDkimSignature.new("DKIM signature is missing required tag b=") unless @dkim_signature.include?('b')
274
- raise InvalidDkimSignature.new("DKIM signature b= value is not valid base64") unless @dkim_signature['b'].match(/[\s0-9A-Za-z+\/]+=*$/)
275
- raise InvalidDkimSignature.new("DKIM signature is missing required tag h=") unless @dkim_signature.include?('h')
276
-
277
- # body hash (not directly encrypted)
278
- raise InvalidDkimSignature.new("DKIM signature is missing required tag bh=") unless @dkim_signature.include?('bh')
279
- raise InvalidDkimSignature.new("DKIM signature bh= value is not valid base64") unless @dkim_signature['bh'].match(/[\s0-9A-Za-z+\/]+=*$/)
280
-
281
- # domain selector
282
- raise InvalidDkimSignature.new("DKIM signature is missing required tag d=") unless @dkim_signature.include?('d')
283
- raise InvalidDkimSignature.new("DKIM signature is missing required tag s=") unless @dkim_signature.include?('s')
298
+ # these two canonicalization methods are defined in the DKIM RFC
299
+ def self.canonicalize_headers(headers, how)
300
+ if how == "simple"
301
+ # No changes to headers.
302
+ $debuglog.puts "canonicalizing headers with 'simple'" unless $debuglog.nil?
303
+ return headers
304
+ elsif how == "relaxed"
305
+ # Convert all header field names to lowercase.
306
+ # Unfold all header lines.
307
+ # Compress WSP to single space.
308
+ # Remove all WSP at the start or end of the field value (strip).
309
+ $debuglog.puts "canonicalizing headers with 'relaxed'" unless $debuglog.nil?
310
+ headers.map{|k, v| [k.downcase, v.gsub(/\r\n/, '').gsub(/\s+/, " ").strip + "\r\n"] }
311
+ end
312
+ end
313
+ def self.canonicalize_body(body, how)
314
+ if how == "simple"
315
+ $debuglog.puts "canonicalizing body with 'simple'" unless $debuglog.nil?
316
+ # Ignore all empty lines at the end of the message body.
317
+ body.gsub(/(\r\n)+\Z/, "\r\n")
318
+ elsif how == "relaxed"
319
+ $debuglog.puts "canonicalizing body with 'relaxed'" unless $debuglog.nil?
284
320
 
285
- # these are expiration dates, which are not checked above.
286
- raise InvalidDkimSignature.new("DKIM signature t= value is not a valid decimal integer") unless @dkim_signature['t'].nil? || @dkim_signature['t'].match(/\d+$/)
287
- raise InvalidDkimSignature.new("DKIM signature x= value is not a valid decimal integer") unless @dkim_signature['x'].nil? || @dkim_signature['x'].match(/\d+$/)
288
- raise InvalidDkimSignature.new("DKIM signature x= value is less than t= (and must be greater than or equal to t=). (x=#{@dkim_signature['x']}, t=#{@dkim_signature['t']}) ") unless @dkim_signature['x'].nil? || @dkim_signature['x'].to_i >= @dkim_signature['t'].to_i
289
-
290
- # other unimplemented stuff
291
- raise InvalidDkimSignature.new("DKIM signature i= domain is not a subdomain of d= (i=#{@dkim_signature[i]} d=#{@dkim_signature[d]})") if @dkim_signature['i'] && !(@dkim_signature['i'].end_with?(@dkim_signature['d']) || ["@", ".", "@."].include?(@dkim_signature['i'][-@dkim_signature['d'].size-1]))
292
- raise InvalidDkimSignature.new("DKIM signature l= value is invalid") if @dkim_signature['l'] && !@dkim_signature['l'].match(/\d{,76}$/)
293
- raise InvalidDkimSignature.new("DKIM signature q= value is invalid (got \"#{@dkim_signature['q']}\"; expected \"dns/txt\")") if @dkim_signature['q'] && @dkim_signature['q'] != "dns/txt"
321
+ body.gsub(/[\x09\x20]+\r\n/, "\r\n") # Remove all trailing WSP at end of lines.
322
+ .gsub(/[\x09\x20]+/, " ") # Compress non-line-ending WSP to single space.
323
+ .gsub(/(\r\n)+\Z/, "\r\n") # Ignore all empty lines at the end of the message body.
324
+ # POTENTIAL PROBLEM: the python source has /(\r\n)*$/ so the + / * change is dubious
325
+ end
294
326
  end
295
- end
296
327
 
297
- # these two canonicalization methods are defined in the DKIM RFC
298
- def self.canonicalize_headers(headers, how)
299
- if how == "simple"
300
- # No changes to headers.
301
- $debuglog.puts "canonicalizing headers with 'simple'" unless $debuglog.nil?
302
- return headers
303
- elsif how == "relaxed"
304
- # Convert all header field names to lowercase.
305
- # Unfold all header lines.
306
- # Compress WSP to single space.
307
- # Remove all WSP at the start or end of the field value (strip).
308
- $debuglog.puts "canonicalizing headers with 'relaxed'" unless $debuglog.nil?
309
- headers.map{|k, v| [k.downcase, v.gsub(/\r\n/, '').gsub(/\s+/, " ").strip + "\r\n"] }
310
- end
311
328
  end
312
- def self.canonicalize_body(body, how)
313
- if how == "simple"
314
- $debuglog.puts "canonicalizing body with 'simple'" unless $debuglog.nil?
315
- # Ignore all empty lines at the end of the message body.
316
- body.gsub(/(\r\n)+\Z/, "\r\n")
317
- elsif how == "relaxed"
318
- $debuglog.puts "canonicalizing body with 'relaxed'" unless $debuglog.nil?
319
-
320
- body.gsub(/[\x09\x20]+\r\n/, "\r\n") # Remove all trailing WSP at end of lines.
321
- .gsub(/[\x09\x20]+/, " ") # Compress non-line-ending WSP to single space.
322
- .gsub(/(\r\n)+\Z/, "\r\n") # Ignore all empty lines at the end of the message body.
323
- # POTENTIAL PROBLEM: the python source has /(\r\n)*$/ so the + / * change is dubious
324
- end
325
- end
326
-
327
329
  end
328
-
329
330
  if __FILE__ == $0
330
331
  eml = ARGF.read
331
332
  begin
332
- ret = Dkim::Verifier.new(eml).verify!
333
- rescue Dkim::DkimPermFail
333
+ ret = DkimVerify::Verification::Verifier.new(eml).verify!
334
+ rescue DkimVerify::Verification::DkimPermFail
334
335
  STDERR.puts "uh oh, something went wrong, the signature did not verify correctly"
335
336
  exit 1
336
337
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dkimverify
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy B. Merrill
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-04-10 00:00:00.000000000 Z
11
+ date: 2017-05-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parslet
@@ -57,7 +57,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
57
57
  version: '0'
58
58
  requirements: []
59
59
  rubyforge_project:
60
- rubygems_version: 2.5.1
60
+ rubygems_version: 2.6.11
61
61
  signing_key:
62
62
  specification_version: 4
63
63
  summary: A pure-Ruby library for validating/verifying DKIM signatures.