def test_multiple_from_fails(self): # <https://bugs.launchpad.net/dkimpy/+bug/644046> # additional From header fields should cause verify failure hfrom = b'From: "Resident Evil" <*****@*****.**>\r\n' h, b = self.message.split(b'\n\n', 1) for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign(self.message, b"test", b"example.net", self.key, signature_algorithm=b'ed25519-sha256') # adding an unknown header still verifies h1 = h + b'\r\n' + b'X-Foo: bar' message = b'\n\n'.join((h1, b)) res = dkim.verify(sig + message, dnsfunc=self.dnsfunc) self.assertTrue(res) # adding extra from at end should not verify h1 = h + b'\r\n' + hfrom.strip() message = b'\n\n'.join((h1, b)) res = dkim.verify(sig + message, dnsfunc=self.dnsfunc) self.assertFalse(res) # add extra from in front should not verify either h1 = hfrom + h message = b'\n\n'.join((h1, b)) res = dkim.verify(sig + message, dnsfunc=self.dnsfunc) self.assertFalse(res)
def test_dkim_mail(self): settings.AWS_SES_CONFIGURATION_SET = None # DKIM verification uses DNS to retrieve the public key when checking # the signature, so we need to replace the standard query response with # one that always returns the test key. try: import dkim import dns except ImportError: return def dns_query(qname, rdtype): name = dns.name.from_text(qname) response = dns.message.from_text( 'id 1\n;ANSWER\n%s 60 IN TXT "v=DKIM1; p=%s"' %\ (qname, DKIM_PUBLIC_KEY)) return dns.resolver.Answer(name, rdtype, 1, response) dns.resolver.query = dns_query settings.DKIM_DOMAIN = 'example.com' settings.DKIM_PRIVATE_KEY = DKIM_PRIVATE_KEY send_mail('subject', 'body', '*****@*****.**', ['*****@*****.**']) message = self.outbox.pop()['raw_message'] self.assertTrue(dkim.verify(message)) self.assertFalse(dkim.verify(message + 'some additional text')) self.assertFalse( dkim.verify(message.replace('*****@*****.**', '*****@*****.**')))
def test_dkim_mail(self): # DKIM verification uses DNS to retrieve the public key when checking # the signature, so we need to replace the standard query response with # one that always returns the test key. try: import dkim import dns except ImportError: return def dns_query(qname, rdtype): name = dns.name.from_text(qname) response = dns.message.from_text( 'id 1\n;ANSWER\n%s 60 IN TXT "v=DKIM1; p=%s"' %\ (qname, DKIM_PUBLIC_KEY)) return dns.resolver.Answer(name, rdtype, 1, response) dns.resolver.query = dns_query settings.DKIM_DOMAIN = 'example.com' settings.DKIM_PRIVATE_KEY = DKIM_PRIVATE_KEY send_mail('subject', 'body', '*****@*****.**', ['*****@*****.**']) message = self.outbox.pop()['raw_message'] self.assertTrue(dkim.verify(message)) self.assertFalse(dkim.verify(message + 'some additional text')) self.assertFalse(dkim.verify( message.replace('*****@*****.**', '*****@*****.**')))
def test_sendmail_dkim(self): config.config.set('accounting', 'dkim_privkey', os.path.join( os.path.dirname(__file__), 'dkim_test.private')) config.config.set('accounting', 'dkim_domain', 'example.org') config.config.set('accounting', 'smtp_domain', 'test.example.org') config.config.set('accounting', 'smtp_to_filter', '.*@example') pubkey = open(os.path.join( os.path.dirname(__file__), 'dkim_test.txt'),'rb').read() body = u'räksmörgås' subject = u'Räksmörgåsar!' to = u'"Mr. Räksmörgås" <foo@example>' fromaddr, all_rcpts, message = mail.makemail( body, subject=subject, to=to) mail.sendmail(fromaddr, all_rcpts, message) smtp, = self.smtps message = smtp._sendmail[0][2] assert dkim.verify(message, dnsfunc=lambda *_: pubkey) assert b'[email protected]' in message fromaddr, all_rcpts, message = mail.makemail( body, subject=subject, to=to) mail.sendmail(fromaddr, all_rcpts, message, identity='foo') smtp = self.smtps[1] message = smtp._sendmail[0][2] assert dkim.verify(message, dnsfunc=lambda *_: pubkey) assert b'[email protected]' in message
def test_dkim_signature_canonicalization(self): # <https://bugs.launchpad.net/ubuntu/+source/pydkim/+bug/587783> # Relaxed-mode header signing is wrong # <https://bugs.launchpad.net/dkimpy/+bug/939128> # Simple-mode signature header verification is wrong # (should ignore FWS anywhere in signature tag: b=) sample_msg = b"""\ From: [email protected] To: [email protected] Subject: this is my test message """.replace(b'\n', b'\r\n') sample_privkey = b"""\ -----BEGIN RSA PRIVATE KEY----- MIIBOwIBAAJBANmBe10IgY+u7h3enWTukkqtUD5PR52Tb/mPfjC0QJTocVBq6Za/ PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQJAYFUKsD+uMlcFu1D3YNaR EGYGXjJ6w32jYGJ/P072M3yWOq2S1dvDthI3nRT8MFjZ1wHDAYHrSpfDNJ3v2fvZ cQIhAPgRPmVYn+TGd59asiqG1SZqh+p+CRYHW7B8BsicG5t3AiEA4HYNOohlgWan 8tKgqLJgUdPFbaHZO1nDyBgvV8hvWZUCIQDDdCq6hYKuKeYUy8w3j7cgJq3ih922 2qNWwdJCfCWQbwIgTY0cBvQnNe0067WQIpj2pG7pkHZR6qqZ9SE+AjNTHX0CIQCI Mgq55Y9MCq5wqzy141rnxrJxTwK9ABo3IAFMWEov3g== -----END RSA PRIVATE KEY----- """ sample_pubkey = """\ -----BEGIN PUBLIC KEY----- MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ== -----END PUBLIC KEY----- """ for header_mode in [dkim.Relaxed, dkim.Simple]: dkim_header = dkim.sign(sample_msg, b'example', b'canonical.com', sample_privkey, canonicalize=(header_mode, dkim.Relaxed)) # Folding dkim_header affects b= tag only, since dkim.sign folds # sig_value with empty b= before hashing, and then appends the # signature. So folding dkim_header again adds FWS to # the b= tag only. This should be ignored even with # simple canonicalization. # http://tools.ietf.org/html/rfc4871#section-3.5 signed = dkim.fold(dkim_header) + sample_msg result = dkim.verify(signed, dnsfunc=self.dnsfunc, minkey=512) self.assertTrue(result) dkim_header = dkim.fold(dkim_header) # use a tab for last fold to test tab in FWS bug pos = dkim_header.rindex(b'\r\n ') dkim_header = dkim_header[:pos] + b'\r\n\t' + dkim_header[pos + 3:] result = dkim.verify(dkim_header + sample_msg, dnsfunc=self.dnsfunc, minkey=512) self.assertTrue(result)
def test_dkim_signature_canonicalization(self): # <https://bugs.launchpad.net/ubuntu/+source/pydkim/+bug/587783> # Relaxed-mode header signing is wrong # <https://bugs.launchpad.net/dkimpy/+bug/939128> # Simple-mode signature header verification is wrong # (should ignore FWS anywhere in signature tag: b=) sample_msg = b"""\ From: [email protected] To: [email protected] Subject: this is my test message """.replace(b'\n', b'\r\n') sample_privkey = b"""\ -----BEGIN RSA PRIVATE KEY----- MIIBOwIBAAJBANmBe10IgY+u7h3enWTukkqtUD5PR52Tb/mPfjC0QJTocVBq6Za/ PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQJAYFUKsD+uMlcFu1D3YNaR EGYGXjJ6w32jYGJ/P072M3yWOq2S1dvDthI3nRT8MFjZ1wHDAYHrSpfDNJ3v2fvZ cQIhAPgRPmVYn+TGd59asiqG1SZqh+p+CRYHW7B8BsicG5t3AiEA4HYNOohlgWan 8tKgqLJgUdPFbaHZO1nDyBgvV8hvWZUCIQDDdCq6hYKuKeYUy8w3j7cgJq3ih922 2qNWwdJCfCWQbwIgTY0cBvQnNe0067WQIpj2pG7pkHZR6qqZ9SE+AjNTHX0CIQCI Mgq55Y9MCq5wqzy141rnxrJxTwK9ABo3IAFMWEov3g== -----END RSA PRIVATE KEY----- """ sample_pubkey = """\ -----BEGIN PUBLIC KEY----- MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANmBe10IgY+u7h3enWTukkqtUD5PR52T b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ== -----END PUBLIC KEY----- """ for header_mode in [dkim.Relaxed, dkim.Simple]: dkim_header = dkim.sign(sample_msg, b'example', b'canonical.com', sample_privkey, canonicalize=(header_mode, dkim.Relaxed)) # Folding dkim_header affects b= tag only, since dkim.sign folds # sig_value with empty b= before hashing, and then appends the # signature. So folding dkim_header again adds FWS to # the b= tag only. This should be ignored even with # simple canonicalization. # http://tools.ietf.org/html/rfc4871#section-3.5 signed = dkim.fold(dkim_header) + sample_msg result = dkim.verify(signed,dnsfunc=self.dnsfunc, minkey=512) self.assertTrue(result) dkim_header = dkim.fold(dkim_header) # use a tab for last fold to test tab in FWS bug pos = dkim_header.rindex(b'\r\n ') dkim_header = dkim_header[:pos]+b'\r\n\t'+dkim_header[pos+3:] result = dkim.verify(dkim_header + sample_msg, dnsfunc=self.dnsfunc, minkey=512) self.assertTrue(result)
def test_add_body_length(self): sig = dkim.sign( self.message, b"test", b"example.com", self.key, length=True) msg = email.message_from_string(self.message.decode('utf-8')) self.assertIn('; l=%s' % len(msg.get_payload() + '\n'), sig.decode('utf-8')) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertTrue(res)
def test_email_signed(self): """Verify e-mail signature.""" out = StringIO() email = EmailMessage('Subject', 'Content', '*****@*****.**', ['*****@*****.**']) sent = get_connection(stream=out).send_messages([email]) self.assertEqual(sent, 1) match = re.match('^(.*)\n-{79}\n$', out.getvalue(), flags=re.DOTALL) self.assertTrue(dkim.verify(bytes(match.group(1), 'utf-8'), dnsfunc=lambda x: DNS_TXT_RECORD))
def test_dkim_signature_canonicalization(self): # <https://bugs.launchpad.net/ubuntu/+source/pydkim/+bug/587783> # Relaxed-mode header signing is wrong # <https://bugs.launchpad.net/dkimpy/+bug/939128> # Simple-mode signature header verification is wrong # (should ignore FWS anywhere in signature tag: b=) sample_msg = b"""\ From: [email protected] To: [email protected] Subject: this is my test message """.replace(b'\n', b'\r\n') sample_privkey = b"""\ fL+5V9EquCZAovKik3pA6Lk9zwCzoEtjIuIqK9ZXHHA=\ """ sample_pubkey = """\ yi50DjK5O9pqbFpNHklsv9lqaS0ArSYu02qp1S0DW1Y=\ """ for header_mode in [dkim.Relaxed, dkim.Simple]: dkim_header = dkim.sign(sample_msg, b'example', b'canonical.com', sample_privkey, canonicalize=(header_mode, dkim.Relaxed), signature_algorithm=b'ed25519-sha256') # Folding dkim_header affects b= tag only, since dkim.sign folds # sig_value with empty b= before hashing, and then appends the # signature. So folding dkim_header again adds FWS to # the b= tag only. This should be ignored even with # simple canonicalization. # http://tools.ietf.org/html/rfc4871#section-3.5 signed = dkim.fold(dkim_header) + sample_msg result = dkim.verify(signed, dnsfunc=self.dnsfunc) self.assertTrue(result) dkim_header = dkim.fold(dkim_header) # use a tab for last fold to test tab in FWS bug pos = dkim_header.rindex(b'\r\n ') dkim_header = dkim_header[:pos] + b'\r\n\t' + dkim_header[pos + 3:] result = dkim.verify(dkim_header + sample_msg, dnsfunc=self.dnsfunc) self.assertTrue(result)
def test_verifies(self): # A message verifies after being signed. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo)) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertTrue(res)
def test_altered_body_fails(self): # An altered body fails verification. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign(self.message, b"test", b"example.com", self.key) res = dkim.verify(sig + self.message + b"foo", dnsfunc=self.dnsfunc) self.assertFalse(res)
def test_badly_encoded_domain_fails(self): # Domains should be ASCII. Bad ASCII causes verification to fail. sig = dkim.sign(self.message, b"test", b"example.net\xe9", self.key, signature_algorithm=b'ed25519-sha256') res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertFalse(res)
def main(): msg = sys.stdin.read() res = None res = dkim.verify(msg) print('[' + os.path.basename(__file__) + '] isDkimValid = ' + str(res)) if not res: # Invalid signature, exit with code 11. sys.exit(11)
def test_altered_body_fails(self): # An altered body fails verification. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key) res = dkim.verify( sig + self.message + b"foo", dnsfunc=self.dnsfunc) self.assertFalse(res)
def test_simple_signature(self): # A message verifies after being signed with SHOULD headers for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo), include_headers=(b'from',) + dkim.DKIM.SHOULD) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertTrue(res)
def verify_dkim_sig(crypto_message): ''' Verify DKIM signature if option selected and header exists. ''' verified_sig = False if crypto_message.get_email_message().get_header( 'DKIM-Signature') is not None: log_message('trying to verify DKIM signature') try: global _log crypto_message.set_dkim_signed(True) charset, __ = get_charset(crypto_message.get_email_message()) log_message('dkim message char set: {}'.format(charset)) message = crypto_message.get_email_message().to_string().encode() if DEBUGGING: log_message('headers before DKIM verification:\n{}'.format( crypto_message.get_email_message().get_header_lines())) log_message('message:\n{}'.format(message)) if DKIM_VERIFICATION_ACTIVE: verified_sig = dkim.verify(message, logger=_log) log_message('DKIM signature verified: {}'.format(verified_sig)) if verified_sig: crypto_message.get_email_message().delete_header( 'DKIM-Signature') crypto_message.set_dkim_sig_verified(True) elif options.dkim_delivery_policy() == DKIM_WARN_POLICY: crypto_message.get_email_message().delete_header( 'DKIM-Signature') log_message('dkim policy is to warn and accept message') else: raise DKIMException( "Unable to verify message originated on sender's mail server." ) else: verified_sig = True # !!!!! fix dkim in python3 log_message('unable to verify dkim sig with python3') except dkim.DKIMException as dkim_exception: if options.dkim_delivery_policy() == DKIM_WARN_POLICY: crypto_message.get_email_message().delete_header( 'DKIM-Signature') log_message( 'dkim policy is to warn; {}'.format(dkim_exception)) else: raise dkim.DKIMException(str(dkim_exception)) except: log_message('EXCEPTION - see syr.exception.log for details') record_exception() else: verified_sig = False return crypto_message, verified_sig
def test_unknown_k(self): # A error is detected if an unknown algorithm is in the k= tag. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign(self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo)) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc4) self.assertFalse(res)
def test_bad_version(self): # A error is detected if a bad version is used. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign(self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo)) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc3) self.assertFalse(res)
def test_ignores_tlsrptsvc(self): # A non-tlsrpt signed with a key record with s=tlsrpt shouldn't verify. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign(self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo)) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc6) self.assertFalse(res)
def verify_dkim(path): ''' Verify DKIM signature of an e-mail file. :param path: Path to the e-mail file. :returns: Whether DKIM signature is valid or not. ''' with open(path, 'rb') as message_file: message_bytes = message_file.read() return dkim.verify(message_bytes)
def test_double_verifies(self): # A message also containing a ed25519 signature verifies after being signed with rsa. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign(self.message3, b"test", b"football.example.com", self.key, canonicalize=(header_algo, body_algo), signature_algorithm=b'rsa-sha256') res = dkim.verify(sig + self.message3, dnsfunc=self.dnsfunc5) self.assertTrue(res)
def test_rfc8032_verifies(self): # A message using RFC 8032 sample keys verifies after being signed. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign(self.message3, b"brisbane", b"football.example.com", self.rfckey, canonicalize=(header_algo, body_algo), signature_algorithm=b'ed25519-sha256') res = dkim.verify(sig + self.message3, dnsfunc=self.dnsfunc) self.assertTrue(res)
def test_string_include(self): # A message can be signed when the include_headers is string for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign(self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo), include_headers=('from', )) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertTrue(res)
def test_l_verify(self): # Sign with l=, add text, should verify for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign(self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo), length=True) self.message += b'added more text\n' res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertTrue(res)
def test_tlsrpt_with_no_tlsrptsvc(self): # A tlsrpt signed with a key record without s=tlsrpt and tlsrpt=True should verify. for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign(self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo)) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc, tlsrpt=True) self.assertTrue(res)
def test_email_signed(self, mock_smtp): """Test sending signed email over SMTP.""" email = EmailMessage('Subject', 'Content', '*****@*****.**', ['*****@*****.**']) sent = get_connection().send_messages([email]) self.assertEqual(sent, 1) instance = mock_smtp.return_value self.assertEqual(instance.sendmail.call_count, 1) args, kwargs = instance.sendmail.call_args self.assertEqual(args[0], '*****@*****.**') self.assertEqual(args[1], ['*****@*****.**']) self.assertTrue(dkim.verify(args[2], dnsfunc=lambda x: DNS_TXT_RECORD))
def _test_email_with_dkim(include_headers): from yagmail import SMTP from yagmail.dkim import DKIM private_key_path = Path(__file__).parent / "privkey.pem" private_key = private_key_path.read_bytes() dkim_obj = DKIM( domain=b"a.com", selector=b"selector", private_key=private_key, include_headers=include_headers, ) yag = SMTP( user="******", host="smtp.blabla.com", port=25, dkim=dkim_obj, ) yag.login = Mock() to = "*****@*****.**" recipients, msg_bytes = yag.send(to=to, subject="hello from tests", contents="important message", preview_only=True) msg_string = msg_bytes.decode("utf8") assert recipients == [to] assert "Subject: hello from tests" in msg_string text_b64 = base64.b64encode(b"important message").decode("utf8") assert text_b64 in msg_string dkim_string1 = "DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=a.com; [email protected];\n " \ "q=dns/txt; s=selector; t=" assert dkim_string1 in msg_string l = logging.getLogger() l.setLevel(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG) assert dkim.verify(message=msg_string.encode("utf8"), logger=l, dnsfunc=get_txt_from_test_file) return msg_string
def test_tlsrpt_ignore_l_sign(self): # For a tlsrpt, don't add l= when signing tlsrpt for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign(self.message, b"test", b"example.com", self.key, canonicalize=(header_algo, body_algo), length=True, tlsrpt=True) self.message += b'added more text' res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertFalse(res)
def verify_dkim(path): """ Verify DKIM signature of an e-mail file. :param path: Path to the e-mail file. :returns: Whether DKIM signature is valid or not. """ with open(path, 'rb') as message_file: message_bytes = message_file.read() try: return dkim.verify(message_bytes) except (dns.exception.DNSException, dkim.DKIMException) as exception: raise DKIMVerifyError(str(exception)) from exception
def test_multiple_from_fails(self): # <https://bugs.launchpad.net/dkimpy/+bug/644046> # additional From header fields should cause verify failure hfrom = b'From: "Resident Evil" <*****@*****.**>\r\n' h,b = self.message.split(b'\n\n',1) for header_algo in (b"simple", b"relaxed"): for body_algo in (b"simple", b"relaxed"): sig = dkim.sign( self.message, b"test", b"example.com", self.key) # adding an unknown header still verifies h1 = h+b'\r\n'+b'X-Foo: bar' message = b'\n\n'.join((h1,b)) res = dkim.verify(sig+message, dnsfunc=self.dnsfunc) self.assertTrue(res) # adding extra from at end should not verify h1 = h+b'\r\n'+hfrom.strip() message = b'\n\n'.join((h1,b)) res = dkim.verify(sig+message, dnsfunc=self.dnsfunc) self.assertFalse(res) # add extra from in front should not verify either h1 = hfrom+h message = b'\n\n'.join((h1,b)) res = dkim.verify(sig+message, dnsfunc=self.dnsfunc) self.assertFalse(res)
def verify_dkim_sig(crypto_message): ''' Verify DKIM signature if option selected and header exists. ''' verified_sig = False if crypto_message.get_email_message().get_header('DKIM-Signature') is not None: log_message('trying to verify DKIM signature') try: global _log crypto_message.set_dkim_signed(True) charset, __ = get_charset(crypto_message.get_email_message()) log_message('dkim message char set: {}'.format(charset)) message = crypto_message.get_email_message().to_string().encode() if DEBUGGING: log_message('headers before DKIM verification:\n{}'.format( crypto_message.get_email_message().get_header_lines())) log_message('message:\n{}'.format(message)) if DKIM_VERIFICATION_ACTIVE: verified_sig = dkim.verify(message, logger=_log) log_message('DKIM signature verified: {}'.format(verified_sig)) if verified_sig: crypto_message.get_email_message().delete_header('DKIM-Signature') crypto_message.set_dkim_sig_verified(True) elif options.dkim_delivery_policy() == DKIM_WARN_POLICY: crypto_message.get_email_message().delete_header('DKIM-Signature') log_message('dkim policy is to warn and accept message') else: raise DKIMException("Unable to verify message originated on sender's mail server.") else: verified_sig = True # !!!!! fix dkim in python3 log_message('unable to verify dkim sig with python3') except dkim.DKIMException as dkim_exception: if options.dkim_delivery_policy() == DKIM_WARN_POLICY: crypto_message.get_email_message().delete_header('DKIM-Signature') log_message('dkim policy is to warn; {}'.format(dkim_exception)) else: raise dkim.DKIMException(str(dkim_exception)) except: log_message('EXCEPTION - see syr.exception.log for details') record_exception() else: verified_sig = False return crypto_message, verified_sig
def test_dkim(self): privkey = self._get_dkim_privkey() mailing = factories.MailingFactory(dkim={'selector': 'mail', 'domain': 'unittest.cloud-mailing.net', 'privkey':privkey}) recipient = factories.RecipientFactory(mailing=mailing) message_str = self._customize(recipient) self.assertNotIn(b"\r\n", message_str) parser = email.parser.Parser() message = parser.parsestr(message_str, headersonly=False) assert (isinstance(message, email.message.Message)) self.assertTrue('DKIM-Signature' in message) # print message['DKIM-Signature'] self.assertTrue(dkim.verify(message_str, dnsfunc=self._get_txt))
def main(): if sys.version_info[0] >= 3: # Make sys.stdin a binary stream. sys.stdin = sys.stdin.detach() message = sys.stdin.read() verbose = '-v' in sys.argv if verbose: import logging d = dkim.DKIM(message, logger=logging) res = d.verify() else: res = dkim.verify(message) if not res: print("signature verification failed") sys.exit(1) print("signature ok")
def dkim_verify(self, msg_str, norm_addr): # DKIM verification. Simply check that the server has verified the # message's signature if self.dkim: log.msg("Checking DKIM signature.", system="email parser") # Note: msg.as_string() changes the message to conver it to # string, so DKIM will fail. Use the original string instead if dkim.verify(msg_str): log.msg("Valid DKIM signature.", system="email parser") return True else: log.msg("Invalid DKIM signature.", system="email parser") username, domain = norm_addr.split("@") raise DkimError("DKIM failed for {} at {}".format( hid.hexdigest(), domain)) # Is this even useful like this? else: return True
async def handle_DATA(self, server: SMTP, session: Session, envelope: Envelope): valid_dkim = dkim.verify(envelope.content) envelope.dkim = valid_dkim log.info("DKIM: %s", test_results[valid_dkim]) message: Message = email.message_from_bytes(envelope.content) log.info('From: %s', message['From']) log.info('To: %s', message['To']) log.info('Subject: %s', message['Subject']) log.info('Message data:\n%s', message_to_display(message)) if self.verify_dkim and not valid_dkim: return '550 DKIM validation failed' return '250 Message accepted for delivery'
def verify_dkim(self, body, options, output): """ Purpose: Verify DKIM Signature Returns: Status, Message """ match = re.search('d=[^;]+; s=(\d{14});', body) if not match: # Enable the next line, if no dkim signature should only be a warning. # return RETURN_WARNING, "No dkim signature found" return RETURN_CRITICAL, "No dkim signature found" selector = match.group(1) if dkim.verify(body): #, debuglog=output): if (datetime.now() - datetime.strptime(selector, '%Y%m%d%H%M%S')) > timedelta(days=KEY_MAX_AGE): return RETURN_CRITICAL, 'DKIM key (%s) older than %s days' % (selector, KEY_MAX_AGE) return RETURN_OK, 'DKIM verification successful, selector is %s' % selector else: return RETURN_CRITICAL, 'DKIM verification failed'
def isSenderVerified(message): # check 1: DKIM email_message = message.original _, sender_addr = parseaddr(message['From'].lower()) _, to_addr = parseaddr(message['To'].lower()) verified = dkim.verify(email_message) # import spf # check 2: SPF - TODO: SPF not implemented - need to find incoming mail server IP address # if not verified: # spf_i = "" # spf_h = "" # spf_s = sender_addr # result = spf.check(spf_i, spf_s, spf_h) # if result[0] == "pass": # verified = True if not verified: # check if UserProfile has a hash, if not generate one and send it to them try: user = UserProfile.objects.get(email=sender_addr) except UserProfile.DoesNotExist: user = None if user: # TODO: create new user here if it doesn't exist or otherwise handle posts from non-users # for now just mark as un-verified if DKIM and SPF fail if not user.hash: salt = hashlib.sha1(str(random.random())+str(time.time())).hexdigest()[:5] new_hash = hashlib.sha1(sender_addr+to_addr+salt).hexdigest()[:20] user.hash = new_hash user.save() #mail = MurmurMailResponse(From = NO_REPLY, Subject = "Please use your secret code in future emails") #mail.Body = "In future, to ensure your message is delivered, please include the code %s within the address of your emails, before the '@' symbol and after a '+' symbol. E.g. if you are emailing testgroup@%s, you should now email testgroup+%s@%s to ensure your email is verified as coming directly from you, and thus delivered correctly." % (new_hash, HOST, new_hash, HOST) #relay.deliver(mail, To = sender_addr) hash_group = re.search(r'\+(.{20,40}?)\@', to_addr) if hash_group: sender_hash = hash_group.group(1) if sender_hash == user.hash: verified = True return verified
f.write( query + str(testid) + str(unixtime) + "\n") f.write( str(test) + "\n") if (len(test) >= 1) and int(test[0][0]) == 1: timestamp=test[0][1] parent = testid query = "INSERT INTO queue (domain, email, test_id, parent, start_time, ip, extra, slow) VALUES(%s, %s, nextval('test_id_seq'), %s, %s, %s, 'd:StandardAddresses', true) RETURNING test_id, EXTRACT(EPOCH FROM start_time)::integer AS start_time" test=db.fetch(query, ( domain, email, testid, timestamp, db_ip )) testid = test[0][0] f.write(str(test)+"\n") else: exit(str("No matching test")) f.write(str(output)+"\n"+str(query)+"\n") if len(extra_tests) >= 1: for extra in extra_tests: if extra == 'DKIM' and incoming == False: dkim=dkim.verify(output) if dkim == True: message='DKIM test successful' goldstar='1' status='1' else: message='DKIM-test failed' goldstar='0' status='2' save_result(testid, message, status, 'adv', 'DKIM', 'Incoming', '0', goldstar, int()) save_result(testid, message, status, 'adv', 'DKIM', 'Incoming', '1', goldstar, int())
# 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2008 Greg Hewgill http://hewgill.com # # This has been modified from the original software. # Copyright (c) 2011 William Grant <*****@*****.**> from __future__ import print_function import sys import dkim if sys.version_info[0] >= 3: # Make sys.stdin a binary stream. sys.stdin = sys.stdin.detach() message = sys.stdin.read() verbose = '-v' in sys.argv if verbose: d = dkim.DKIM(message) res = d.verify() else: res = dkim.verify(message) if not res: print("signature verification failed") sys.exit(1) print("signature ok")
from_address = re.search("<(.+)>", from_address).group(1) #Get Domain from email address from_address = str(from_address.lower()) (user, domain) = from_address.split("@") # Get spf result from email spf_result = re.search('spf=(\S+)', msg).group(1) # Get DKIM result dkim_present = re.search('^DKIM-Signature', msg, re.MULTILINE) # If we have a DKIM signature, verify it. If not, DKIM = none if dkim_present: try: dkim_result = dkim.verify(msg) except DKIMException as x: print('DKIM Exception: ' + str(x)) else: dkim_result = 'none' # Evaluate results according to our rules # DKIM valid, SPF pass if dkim_result == True and spf_result == 'pass': results['dkim_valid_spf_pass'] += 1 # DKIM valid, SPF none elif dkim_result == True and (spf_result == 'neutral' or spf_result == 'none'): results['dkim_valid_spf_none'] += 1 # DKIM valid, SPF fail elif dkim_result == True and (spf_result == 'fail' or spf_result == 'softfail' or spf_result == 'temperror' or spf_result == 'permerror'):
#!/usr/local/bin/python2.5 # This software is provided 'as-is', without any express or implied # warranty. In no event will the author be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # # Copyright (c) 2008 Greg Hewgill http://hewgill.com import sys import dkim message = sys.stdin.read() if not dkim.verify(message): print "signature verification failed" sys.exit(1) print "signature ok"
def checkMail (mailbox, outputFileHandle): htmlDMARCBox = "" htmlDKIMBox = "" htmlSPFBox = "" #Read the message from the inbox mailBoxFileHandle = open(mailbox,'r+') message = mailBoxFileHandle.read() headers = Parser().parsestr(message) receivedHeader = headers['Received'] #print headers.items() pattern = re.compile(r'\[.*]') result = re.search(pattern, receivedHeader).group(0) pattern = re.compile(r'from .*? ') result2 = re.search(pattern, receivedHeader).group(0) subject = headers['subject'] #Variables needed for spf check fromHeader = headers['from'] ipaddr = result[1:-1] host = result2[5:-1] # Perfom SPF and DKIM checks spfResult = spf.check(i=ipaddr,s=fromHeader,h=host) dkimResult = dkim.verify(message ,None) #Create HTML conentent according to the results of the test if (spfResult[0] == 'pass'): htmlSPFBox = """<tr class="success"> <td>SPF check passed <br><strong>↳</strong>""" + spfResult[2] + """</td> </tr>""" else: htmlSPFBox = """<tr class="danger"> <td>SPF check failed <br><strong>↳</strong>""" + spfResult[2] + """</td> </tr>""" if (dkimResult == True): htmlDKIMBox = """<tr class="success"> <td>DKIM check passed</td> </tr>""" else: htmlDKIMBox = """<tr class="danger"> <td>DKIM check failed</td> </tr>""" if (spfResult[0] == 'pass' or dkimResult == True): htmlDMARCBox = """<tr class="success"> <td>DMARC check passed</td> </tr>""" else: htmlDMARCBox = """<tr class="danger"> <td>DMARC check failed</td> </tr>""" html = """ <div class="well well-lg"> <h4><p class="text-center"><strong>DMARC test</strong></p></h4> <table class="table table-condensed"> <thead> <tr> <th>Result</th> </tr> <tr></tr> </thead> <tbody> <tr> <td>Subject: """ + subject + """</td> </tr> <tr><td></td></tr> """ + htmlDKIMBox + htmlSPFBox + """<tr><td></td></tr>""" + htmlDMARCBox + """ </tbody> </table> </div> """ outputFileHandle.write(html) outputFileHandle.close() # Empty the mail box for the the next test, remove this line if # you whish to keep the email. # (Hackish way of emtying the file contents) open(mailbox, 'w').close()
def __validate_mail(message): # First things first (and this is critical), # validate the DKIM signature if not dkim.verify(message): raise ValidateMailException("DKIM signature verification failed\n") # Parse the email (headers, body) = dkim.rfc822_parse(message) # Get the one and only DKIM-Signature header and from address from_addr = __get_raw_email_addr(__get_required_header(headers, "From")) dkim_sig = __get_required_header(headers, "DKIM-Signature") # Check that the from address and the Return-Path address are consistent return_paths = __get_headers_by_name(headers, "Return-Path") if len(return_paths) == 0: raise ValidateMailException("No return paths specified\n") for rp in return_paths: if __get_raw_email_addr(rp) != from_addr: raise ValidateMailException("'Return-Path: " + str(rp) + "' does not match from address.\n") # Check a few things in the DKIM header dkim_fields = parse_dkim_sig(dkim_sig) if 'bh' not in dkim_fields: raise ValidateMailException("Missing the DKIM body hash.\n") if 'h' not in dkim_fields: raise ValidateMailException("Missing DKIM headers key (h=).\n") if 'd' not in dkim_fields: raise ValidateMailException("Missing DKIM domain field.\n") if dkim_fields['d'] != "gmail.com": raise ValidateMailException("Not from gmail.com\n") signed_headers = [fld.lower().strip() for fld in dkim_fields['h'].split(":")] if 'from' not in signed_headers: raise ValidateMailException("From address is not included in signed headers!\n") # Some other magic stuff # NOTE: It is legal for there to be numerous Authentication-Results headers, # but, we won't handle that yet. Instead, just fail if there is more than one. auth_results = __get_required_header(headers, "Authentication-Results").strip() if not re.match(r"^mx\.google\.com;", auth_results): raise ValidateMailException("Authentication-Results header not from mx.google.com\n") # check various features auth_results = " " + re.sub(r"\s+", " ", auth_results).lower().strip() + " " if " dkim=pass " not in auth_results: raise ValidateMailException("Authentication-Results failure: No 'dkim=pass'\n") if " dmarc=pass " not in auth_results: raise ValidateMailException("Authentication-Results failure: No 'dmarc=pass'\n") if " spf=pass " not in auth_results: raise ValidateMailException("Authentication-Results failure: No 'spf=pass'\n") # Try to get the smtp.mailfrom header if not re.match(r"^.*spf=pass [^;]+? smtp.mailfrom=" + re.escape(from_addr) + "(;.*)?$", auth_results): raise ValidateMailException("Authentication-Results failure: invalid or missing smtp.mailfrom address\n") return from_addr
def _verifyDkimOrigin(signed_message): """Find a From or Sender address for which there's a DKIM signature. :returns: A string email address for the trusted sender, if there is one, otherwise None. :param signed_message: ISignedMessage """ log = logging.getLogger('mail-authenticate-dkim') log.setLevel(logging.DEBUG) if getFeatureFlag('mail.dkim_authentication.disabled'): log.info('dkim authentication feature disabled') return None # uncomment this for easier test debugging # log.addHandler(logging.FileHandler('/tmp/dkim.log')) dkim_log = cStringIO() log.info( 'Attempting DKIM authentication of message id=%r from=%r sender=%r' % (signed_message['Message-ID'], signed_message['From'], signed_message['Sender'])) signing_details = [] dkim_result = False try: dkim_result = dkim.verify( signed_message.parsed_string, dkim_log, details=signing_details) except dkim.DKIMException as e: log.warning('DKIM error: %r' % (e,)) except dns.resolver.NXDOMAIN as e: # This can easily happen just through bad input data, ie claiming to # be signed by a domain with no visible key of that name. It's not an # operational error. log.info('DNS exception: %r' % (e,)) except dns.exception.DNSException as e: # many of them have lame messages, thus %r log.warning('DNS exception: %r' % (e,)) except Exception as e: # DKIM leaks some errors when it gets bad input, as in bug 881237. We # don't generally want them to cause the mail to be dropped entirely # though. It probably is reasonable to treat them as potential # operational errors, at least until they're handled properly, by # making pydkim itself more defensive. log.warning( 'unexpected error in DKIM verification, treating as unsigned: %r' % (e,)) log.info('DKIM verification result: trusted=%s' % (dkim_result,)) log.debug('DKIM debug log: %s' % (dkim_log.getvalue(),)) if not dkim_result: return None # in addition to the dkim signature being valid, we have to check that it # was actually signed by the user's domain. if len(signing_details) != 1: log.info( 'expected exactly one DKIM details record: %r' % (signing_details,)) return None signing_domain = signing_details[0]['d'] if not _isDkimDomainTrusted(signing_domain): log.info("valid DKIM signature from untrusted domain %s" % (signing_domain,)) return None for origin in ['From', 'Sender']: if signed_message[origin] is None: continue name, addr = parseaddr(signed_message[origin]) try: origin_domain = addr.split('@')[1] except IndexError: log.warning( "couldn't extract domain from address %r", signed_message[origin]) if signing_domain == origin_domain: log.info( "DKIM signing domain %s matches %s address %r", signing_domain, origin, addr) return addr else: log.info("DKIM signing domain %s doesn't match message origin; " "disregarding signature" % (signing_domain)) return None
def scan(fileobj, dkimrule = 0, returnpath = None): try: import asnn_config # Declaring the 'import' where the CLI is starting is not getting it # into the namespace properly and it's faulting in this function. fileobj.seek(0) # to the top of the file domains = [] # 'Return-Path' and 'From' domains # create a parsing object that reads the email from the file object msgobj = email.message_from_file(fileobj) if returnpath == '<>': # the message is a bounce, maybe force DKIM to make # sure that it's not a spam masquarading as a bounce if asnn_config.DKIM_FORCE_ON_BOUNCE: ############# THIS FUNCTIONALITY HAS NOT BEEN TESTED ################## dkimrule = 2 elif returnpath: domain = returnpath.split('@')[1] if debug > 5: print >> sys.stderr, logid() + "DKIM: 'Return-Path' domain: " + domain dobj = asnn_whois_domain.getbasedomain(domain, retobject=True, retupper=True) # (By 'returning upper', adding a subdomain to a domain that should be # checked will get checked. Domains that don't exist in the DB will # end up returning a TLD, but no TLD should have a DKIM rule.) if dobj: # don't look unless we found a result for line in dobj.whoisdecode.splitlines(): if line == 'rpskipdkim': if debug > 4: print >> sys.stderr, logid() + \ "DKIM: check bypassed per return-path domain: " + dobj.domain return '200 DKIM check bypassed' if line == 'forcedkim': dkimrule = 2 if debug > 4: print >> sys.stderr, logid() + "DKIM: forced check per RP domain: " + dobj.domain break if line == 'checkdkim' and dkimrule < 2: dkimrule = 1 if debug > 4: print >> sys.stderr, logid() + "DKIM: check per RP domain: " + dobj.domain break if ('from' in msgobj) and ('@' in msgobj['From']): if '<' in msgobj['From']: domain = msgobj['From'].split('<')[1].split('@')[1].split('>')[0] else: domain = msgobj['From'].split('@')[1] dobj = asnn_whois_domain.getbasedomain(domain, retobject=True, retupper=True) if debug > 5: print >> sys.stderr, logid() + "DKIM: 'From' domain: " + domain if dobj: # don't look unless we found a result for line in dobj.whoisdecode.splitlines(): if line == 'forcedkim': dkimrule = 2 if debug > 4: print >> sys.stderr, logid() + "DKIM: forced check per PRA domain: " + dobj.domain break if line == 'checkdkim' and dkimrule < 2: dkimrule = 1 if debug > 4: print >> sys.stderr, logid() + "DKIM: check per PRA domain: " + dobj.domain break else: if debug > 3: print >> sys.stderr, logid() + "DKIM: bad or missing 'From'" if not dkimrule: if debug > 4: print >> sys.stderr, logid() + "DKIM: no DKIM check required" return '200 no DKIM check required' import dkim if 'DKIM-Signature' not in msgobj: if debug > 4: print >> sys.stderr, logid() + "DKIM: no DKIM header found" if dkimrule == 2: return asnn_config.REJECT_DKIM return '200 no DKIM header found' # i.e. dkimrule == 1 fileobj.seek(0) # to the top of the file dkimresult = dkim.verify(fileobj.read()) if not dkimresult: if debug > 4: print >> sys.stderr, logid() + "DKIM: reject per result" return asnn_config.REJECT_DKIM if debug > 4: print >> sys.stderr, logid() + "DKIM: result okay" return '200 DKIM check okay' except Exception as msg: if debug > 0: print >> sys.stderr, logid() + "DKIM: exception during check: " + str(msg) if debug > 4: import traceback print >> sys.stderr, traceback.print_exc() return asnn_config.SOFTFAIL_DKIM
def auth(self, message_data=None, peer_ip=None, message=None): return dkim.verify(message_data)
def test_badly_encoded_domain_fails(self): # Domains should be ASCII. Bad ASCII causes verification to fail. sig = dkim.sign(self.message, b"test", b"example.com\xe9", self.key) res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc) self.assertFalse(res)
def checkMail (message): outputFileHandle = None try: outputFileHandle = open('dmarcCheck.html', 'w') except: print "file error" sys.exit() htmlDMARCBox = "" htmlDKIMBox = "" htmlSPFBox = "" receivedHeader = "" fromHeader = "" #Read the message from the inbox headers = email.parser.Parser().parsestr(message) for field in headers.items(): if field[0] == "Received" and "[" in field[1] and "]" in field[1]: receivedHeader = field[1] pattern = re.compile(r'\[.*]') result = re.search(pattern, receivedHeader).group(0) pattern = re.compile(r'from .*? ') result2 = re.search(pattern, receivedHeader).group(0) subject = headers['subject'] #Variables needed for spf check #We need only the email address if "<" in headers['from'] and ">" in headers['from']: pattern = re.compile(r'\<.*>') fromHeader = re.search(pattern, headers['from']).group(0) fromHeader = fromHeader[1:-1] else: fromHeader = headers['from'] ipaddr = result[1:-1] host = result2[5:-1] # Perfom SPF and DKIM checks spfResult = spf.check(i=ipaddr,s=fromHeader,h=host) dkimResult = dkim.verify(message ,None) #Create HTML conentent according to the results of the test if (spfResult[0] == 'pass'): htmlSPFBox = """<tr class="success"> <td>SPF check passed <br><strong>↳</strong>""" + spfResult[2] + """</td> </tr>""" else: htmlSPFBox = """<tr class="danger"> <td>SPF check failed <br><strong>↳</strong>""" + spfResult[2] + """</td> </tr>""" if (dkimResult == True): htmlDKIMBox = """<tr class="success"> <td>DKIM check passed</td> </tr>""" else: htmlDKIMBox = """<tr class="danger"> <td>DKIM check failed</td> </tr>""" if (spfResult[0] == 'pass' or dkimResult == True): htmlDMARCBox = """<tr class="success"> <td>DMARC check passed</td> </tr>""" else: htmlDMARCBox = """<tr class="danger"> <td>DMARC check failed</td> </tr>""" html = """ <div class="well well-lg"> <h4><p class="text-center"><strong>DMARC test</strong></p></h4> <table class="table table-condensed"> <thead> <tr> <th>Result</th> </tr> <tr></tr> </thead> <tbody> <tr> <td>Subject: """ + subject + """</td> </tr> <tr><td></td></tr> """ + htmlDKIMBox + htmlSPFBox + """<tr><td></td></tr>""" + htmlDMARCBox + """ </tbody> </table> </div> """ outputFileHandle.write(html) outputFileHandle.close()