Package dkim :: Package tests :: Module test_dkim
[hide private]
[frames] | no frames]

Source Code for Module dkim.tests.test_dkim

  1  # This software is provided 'as-is', without any express or implied 
  2  # warranty.  In no event will the author be held liable for any damages 
  3  # arising from the use of this software. 
  4  # 
  5  # Permission is granted to anyone to use this software for any purpose, 
  6  # including commercial applications, and to alter it and redistribute it 
  7  # freely, subject to the following restrictions: 
  8  # 
  9  # 1. The origin of this software must not be misrepresented; you must not 
 10  #    claim that you wrote the original software. If you use this software 
 11  #    in a product, an acknowledgment in the product documentation would be 
 12  #    appreciated but is not required. 
 13  # 2. Altered source versions must be plainly marked as such, and must not be 
 14  #    misrepresented as being the original software. 
 15  # 3. This notice may not be removed or altered from any source distribution. 
 16  # 
 17  # Copyright (c) 2011 William Grant <me@williamgrant.id.au> 
 18   
 19  import email 
 20  import os.path 
 21  import unittest 
 22  import time 
 23   
 24  import dkim 
 25   
 26   
27 -def read_test_data(filename):
28 """Get the content of the given test data file. 29 30 The files live in dkim/tests/data. 31 """ 32 path = os.path.join(os.path.dirname(__file__), 'data', filename) 33 with open(path, 'rb') as f: 34 return f.read()
35 36
37 -class TestFold(unittest.TestCase):
38
39 - def test_short_line(self):
40 self.assertEqual( 41 b"foo", dkim.fold(b"foo"))
42
43 - def test_long_line(self):
44 # The function is terribly broken, not passing even this simple 45 # test. 46 self.assertEqual( 47 b"foo" * 24 + b"\r\n foo", dkim.fold(b"foo" * 25))
48 49
50 -class TestSignAndVerify(unittest.TestCase):
51 """End-to-end signature and verification tests.""" 52
53 - def setUp(self):
54 self.message = read_test_data("test.message") 55 self.key = read_test_data("test.private")
56
57 - def dnsfunc(self, domain):
58 sample_dns = """\ 59 k=rsa; \ 60 p=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T\ 61 b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ==""" 62 63 _dns_responses = { 64 'example._domainkey.canonical.com.': sample_dns, 65 'test._domainkey.example.com.': read_test_data("test.txt"), 66 '20120113._domainkey.gmail.com.': """k=rsa; \ 67 p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1Kd87/UeJjenpabgbFwh\ 68 +eBCsSTrqmwIYYvywlbhbqoo2DymndFkbjOVIPIldNs/m40KF+yzMn1skyoxcTUGCQ\ 69 s8g3FgD2Ap3ZB5DekAo5wMmk4wimDO+U8QzI3SD07y2+07wlNWwIt8svnxgdxGkVbb\ 70 hzY8i+RQ9DpSVpPbF7ykQxtKXkv/ahW3KjViiAH+ghvvIhkx4xYSIc9oSwVmAl5Oct\ 71 MEeWUwg8Istjqz8BZeTWbf41fbNhte7Y+YqZOwq1Sd0DbvYAD9NOZK9vlfuac0598H\ 72 Y+vtSBczUiKERHv1yRbcaQtZFh5wtiRrN04BLUTD21MycBX5jYchHjPY/wIDAQAB""" 73 } 74 try: 75 domain = domain.decode('ascii') 76 except UnicodeDecodeError: 77 return None 78 self.assertTrue(domain in _dns_responses,domain) 79 return _dns_responses[domain]
80
81 - def dnsfunc2(self, domain):
82 sample_dns = """\ 83 k=rsa; \ 84 p=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T\ 85 b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ==""" 86 87 _dns_responses = { 88 'example._domainkey.canonical.com.': sample_dns, 89 'test._domainkey.example.com.': read_test_data("test2.txt"), 90 '20120113._domainkey.gmail.com.': """\ 91 p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1Kd87/UeJjenpabgbFwh\ 92 +eBCsSTrqmwIYYvywlbhbqoo2DymndFkbjOVIPIldNs/m40KF+yzMn1skyoxcTUGCQ\ 93 s8g3FgD2Ap3ZB5DekAo5wMmk4wimDO+U8QzI3SD07y2+07wlNWwIt8svnxgdxGkVbb\ 94 hzY8i+RQ9DpSVpPbF7ykQxtKXkv/ahW3KjViiAH+ghvvIhkx4xYSIc9oSwVmAl5Oct\ 95 MEeWUwg8Istjqz8BZeTWbf41fbNhte7Y+YqZOwq1Sd0DbvYAD9NOZK9vlfuac0598H\ 96 Y+vtSBczUiKERHv1yRbcaQtZFh5wtiRrN04BLUTD21MycBX5jYchHjPY/wIDAQAB""" 97 } 98 try: 99 domain = domain.decode('ascii') 100 except UnicodeDecodeError: 101 return None 102 self.assertTrue(domain in _dns_responses,domain) 103 return _dns_responses[domain]
104
105 - def test_verifies(self):
106 # A message verifies after being signed. 107 for header_algo in (b"simple", b"relaxed"): 108 for body_algo in (b"simple", b"relaxed"): 109 sig = dkim.sign( 110 self.message, b"test", b"example.com", self.key, 111 canonicalize=(header_algo, body_algo)) 112 res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) 113 self.assertTrue(res)
114
115 - def test_implicit_k(self):
116 # A message verifies after being signed when k= tag is not provided. 117 for header_algo in (b"simple", b"relaxed"): 118 for body_algo in (b"simple", b"relaxed"): 119 sig = dkim.sign( 120 self.message, b"test", b"example.com", self.key, 121 canonicalize=(header_algo, body_algo)) 122 res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc2) 123 self.assertTrue(res)
124
125 - def test_simple_signature(self):
126 # A message verifies after being signed with SHOULD headers 127 for header_algo in (b"simple", b"relaxed"): 128 for body_algo in (b"simple", b"relaxed"): 129 sig = dkim.sign( 130 self.message, b"test", b"example.com", self.key, 131 canonicalize=(header_algo, body_algo), 132 include_headers=(b'from',) + dkim.DKIM.SHOULD) 133 res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) 134 self.assertTrue(res)
135
136 - def test_add_body_length(self):
137 sig = dkim.sign( 138 self.message, b"test", b"example.com", self.key, length=True) 139 msg = email.message_from_string(self.message.decode('utf-8')) 140 self.assertIn('; l=%s' % len(msg.get_payload() + '\n'), sig.decode('utf-8')) 141 res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) 142 self.assertTrue(res)
143
144 - def test_altered_body_fails(self):
145 # An altered body fails verification. 146 for header_algo in (b"simple", b"relaxed"): 147 for body_algo in (b"simple", b"relaxed"): 148 sig = dkim.sign( 149 self.message, b"test", b"example.com", self.key) 150 res = dkim.verify( 151 sig + self.message + b"foo", dnsfunc=self.dnsfunc) 152 self.assertFalse(res)
153
155 # Domains should be ASCII. Bad ASCII causes verification to fail. 156 sig = dkim.sign(self.message, b"test", b"example.com\xe9", self.key) 157 res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) 158 self.assertFalse(res)
159
161 # <https://bugs.launchpad.net/ubuntu/+source/pydkim/+bug/587783> 162 # Relaxed-mode header signing is wrong 163 # <https://bugs.launchpad.net/dkimpy/+bug/939128> 164 # Simple-mode signature header verification is wrong 165 # (should ignore FWS anywhere in signature tag: b=) 166 sample_msg = b"""\ 167 From: mbp@canonical.com 168 To: scottk@example.com 169 Subject: this is my 170 test message 171 """.replace(b'\n', b'\r\n') 172 173 sample_privkey = b"""\ 174 -----BEGIN RSA PRIVATE KEY----- 175 MIIBOwIBAAJBANmBe10IgY+u7h3enWTukkqtUD5PR52Tb/mPfjC0QJTocVBq6Za/ 176 PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQJAYFUKsD+uMlcFu1D3YNaR 177 EGYGXjJ6w32jYGJ/P072M3yWOq2S1dvDthI3nRT8MFjZ1wHDAYHrSpfDNJ3v2fvZ 178 cQIhAPgRPmVYn+TGd59asiqG1SZqh+p+CRYHW7B8BsicG5t3AiEA4HYNOohlgWan 179 8tKgqLJgUdPFbaHZO1nDyBgvV8hvWZUCIQDDdCq6hYKuKeYUy8w3j7cgJq3ih922 180 2qNWwdJCfCWQbwIgTY0cBvQnNe0067WQIpj2pG7pkHZR6qqZ9SE+AjNTHX0CIQCI 181 Mgq55Y9MCq5wqzy141rnxrJxTwK9ABo3IAFMWEov3g== 182 -----END RSA PRIVATE KEY----- 183 """ 184 185 sample_pubkey = """\ 186 -----BEGIN PUBLIC KEY----- 187 MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T 188 b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ== 189 -----END PUBLIC KEY----- 190 """ 191 192 for header_mode in [dkim.Relaxed, dkim.Simple]: 193 194 dkim_header = dkim.sign(sample_msg, b'example', b'canonical.com', 195 sample_privkey, canonicalize=(header_mode, dkim.Relaxed)) 196 # Folding dkim_header affects b= tag only, since dkim.sign folds 197 # sig_value with empty b= before hashing, and then appends the 198 # signature. So folding dkim_header again adds FWS to 199 # the b= tag only. This should be ignored even with 200 # simple canonicalization. 201 # http://tools.ietf.org/html/rfc4871#section-3.5 202 signed = dkim.fold(dkim_header) + sample_msg 203 result = dkim.verify(signed,dnsfunc=self.dnsfunc, 204 minkey=512) 205 self.assertTrue(result) 206 dkim_header = dkim.fold(dkim_header) 207 # use a tab for last fold to test tab in FWS bug 208 pos = dkim_header.rindex(b'\r\n ') 209 dkim_header = dkim_header[:pos]+b'\r\n\t'+dkim_header[pos+3:] 210 result = dkim.verify(dkim_header + sample_msg, 211 dnsfunc=self.dnsfunc, minkey=512) 212 self.assertTrue(result)
213
214 - def test_degenerate_folding(self):
215 # <http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=711751> 216 # degenerate folding is ugly but legal 217 message = read_test_data("test2.message") 218 dv = dkim.DKIM(message) 219 res = dv.verify(dnsfunc=self.dnsfunc) 220 self.assertTrue(res)
221
222 - def test_extra_headers(self):
223 # <https://bugs.launchpad.net/dkimpy/+bug/737311> 224 # extra headers above From caused failure 225 #message = read_test_data("test_extra.message") 226 message = read_test_data("message.mbox") 227 for header_algo in (b"simple", b"relaxed"): 228 for body_algo in (b"simple", b"relaxed"): 229 d = dkim.DKIM(message) 230 # bug requires a repeated header to manifest 231 d.should_not_sign.remove(b'received') 232 sig = d.sign(b"test", b"example.com", self.key, 233 include_headers=d.all_sign_headers(), 234 canonicalize=(header_algo, body_algo)) 235 dv = dkim.DKIM(sig + message) 236 res = dv.verify(dnsfunc=self.dnsfunc) 237 self.assertEquals(d.include_headers,dv.include_headers) 238 s = dkim.select_headers(d.headers,d.include_headers) 239 sv = dkim.select_headers(dv.headers,dv.include_headers) 240 self.assertEquals(s,sv) 241 self.assertTrue(res)
242
243 - def test_multiple_from_fails(self):
244 # <https://bugs.launchpad.net/dkimpy/+bug/644046> 245 # additional From header fields should cause verify failure 246 hfrom = b'From: "Resident Evil" <sales@spammer.com>\r\n' 247 h,b = self.message.split(b'\n\n',1) 248 for header_algo in (b"simple", b"relaxed"): 249 for body_algo in (b"simple", b"relaxed"): 250 sig = dkim.sign( 251 self.message, b"test", b"example.com", self.key) 252 # adding an unknown header still verifies 253 h1 = h+b'\r\n'+b'X-Foo: bar' 254 message = b'\n\n'.join((h1,b)) 255 res = dkim.verify(sig+message, dnsfunc=self.dnsfunc) 256 self.assertTrue(res) 257 # adding extra from at end should not verify 258 h1 = h+b'\r\n'+hfrom.strip() 259 message = b'\n\n'.join((h1,b)) 260 res = dkim.verify(sig+message, dnsfunc=self.dnsfunc) 261 self.assertFalse(res) 262 # add extra from in front should not verify either 263 h1 = hfrom+h 264 message = b'\n\n'.join((h1,b)) 265 res = dkim.verify(sig+message, dnsfunc=self.dnsfunc) 266 self.assertFalse(res)
267
268 - def test_no_from_fails(self):
269 # Body From is mandatory to be in the message and mandatory to sign 270 sigerror = False 271 sig = '' 272 message = read_test_data('test_nofrom.message') 273 selector = 'test' 274 domain = 'example.com' 275 identity = None 276 try: 277 sig = dkim.sign(message, selector, domain, read_test_data('test.private'), identity = identity) 278 except dkim.ParameterError as x: 279 sigerror = True 280 self.assertTrue(sigerror)
281
283 sig = {b'v': b'1', 284 b'a': b'rsa-sha256', 285 b'b': b'K/UUOt8lCtgjp3kSTogqBm9lY1Yax/NwZ+bKm39/WKzo5KYe3L/6RoIA/0oiDX4kO\n \t Qut49HCV6ZUe6dY9V5qWBwLanRs1sCnObaOGMpFfs8tU4TWpDSVXaNZAqn15XVW0WH\n \t EzOzUfVuatpa1kF4voIgSbmZHR1vN3WpRtcTBe/I=', 286 b'bh': b'n0HUwGCP28PkesXBPH82Kboy8LhNFWU9zUISIpAez7M=', 287 b'c': b'simple/simple', 288 b'd': b'kitterman.com', 289 b'i': b'scott@Kitterman.com', 290 b'h': b'From:To:Subject:Date:Cc:MIME-Version:Content-Type:\n \t Content-Transfer-Encoding:Message-Id', 291 b's': b'2007-00', 292 b't': b'1299525798'} 293 dkim.validate_signature_fields(sig) 294 # try new version 295 sigVer = sig.copy() 296 sigVer[b'v'] = 2 297 self.assertRaises(dkim.ValidationError, dkim.validate_signature_fields, sigVer) 298 # try with x 299 sigX = sig.copy() 300 sigX[b'x'] = b'1399525798' 301 dkim.validate_signature_fields(sig) 302 # try with late t 303 sigX[b't'] = b'1400000000' 304 self.assertRaises(dkim.ValidationError, dkim.validate_signature_fields, sigX) 305 # try without t 306 now = int(time.time()) 307 sigX[b'x'] = str(now+400000).encode('ascii') 308 dkim.validate_signature_fields(sigX) 309 # try when expired a day ago 310 sigX[b'x'] = str(now - 24*3600).encode('ascii') 311 self.assertRaises(dkim.ValidationError, dkim.validate_signature_fields, sigX)
312
313 -def test_suite():
314 from unittest import TestLoader 315 return TestLoader().loadTestsFromName(__name__)
316