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.
- checksums.yaml +4 -4
- data/dkimverify.gemspec +1 -1
- data/dkimverify.rb +273 -272
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 72a972bced6eedcae611a115ee46c5aceb7f3230
|
4
|
+
data.tar.gz: 88672a73ab1f4a176d845246afa082d31e15b607
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 015d9d955eb0a8ac8c03146e7ee1eace2858d665a3174597d26af7ada3958f97ab7739114c37cea74cd311ec75acc2255d5d5b5e62731a782ec465f034b00d53
|
7
|
+
data.tar.gz: 8898575eb90534f1404f1148133efe6297440cdd7c152d2060c9491c369ea0d9dc11bbfc8671b6879a9d996d8a37a64bcfa38a0b9b005661733fbab10803b3dc
|
data/dkimverify.gemspec
CHANGED
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
|
10
|
-
|
9
|
+
module DkimVerify
|
10
|
+
module Mail
|
11
|
+
class MessageFormatError < StandardError; end
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
class HeaderHash < Hash
|
14
|
+
def get(header_name)
|
15
|
+
self[get_name(header_name)]
|
16
|
+
end
|
16
17
|
|
17
|
-
|
18
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
31
|
+
def headers
|
32
|
+
self.parse! unless @parsed
|
33
|
+
@headers
|
34
|
+
end
|
34
35
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
36
|
+
def body
|
37
|
+
self.parse! unless @parsed
|
38
|
+
@body
|
39
|
+
end
|
39
40
|
|
40
|
-
|
41
|
-
|
41
|
+
def parse!
|
42
|
+
"""Parse a message in RFC822 format.
|
42
43
|
|
43
|
-
|
44
|
+
@param message: The message in RFC822 format. Either CRLF or LF is an accepted line separator.
|
44
45
|
|
45
|
-
|
46
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
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
|
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
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
122
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
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
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
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
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
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
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
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
|
-
|
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
|
-
|
201
|
-
|
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
|
-
|
211
|
-
[
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
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
|
-
|
218
|
-
|
219
|
-
|
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
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
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
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
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
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
# header
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
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
|
-
#
|
286
|
-
|
287
|
-
|
288
|
-
|
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 =
|
333
|
-
rescue
|
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.
|
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-
|
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.
|
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.
|