dkimverify 0.0.6 → 0.0.7

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