@@ -0,0 +1,192 @@
#!/usr/bin/env python
import os
import grp
import sys
import stat
import time
import hmac
import boto3
import tempfile
from datetime import datetime , timedelta
from hashlib import md5 , sha1 , sha256
from Crypto .PublicKey import RSA
from base64 import b64encode as b64e , b64decode as b64d
from binascii import hexlify , unhexlify
DOMAIN = sys .argv [1 ].rstrip ('.' )
SECRET = 'PUY_YOUR_RANDOM_SECRET_HERE'
BASEDIR = '/etc/exim4/dkim_keys'
# In the exim "remote_smpt" transport set the following (assuming Debian):
# DKIM_DOMAIN = ${domain:$return_path}
# DKIM_HMAC = PUT_YOUR_RANDOM_SECRET_HERE
# DKIM_DATE = ${substr{0}{8}{$tod_logfile}}
# DKIM_SELECTOR = DKIM_DATE-${substr{0}{23}{${hmac{sha1}{DKIM_HMAC}{DKIM_DATE}}}}
# DKIM_FILE = /etc/exim4/dkim_keys/${lc:DKIM_SELECTOR}_${lc:DKIM_DOMAIN}.key
# DKIM_PRIVATE_KEY = ${if exists{DKIM_FILE}{DKIM_FILE}{0}}
cli = boto3 .Session (profile_name = 'dkim' ).client ('route53' )
zone_name = '_domainkey.' + DOMAIN + '.'
zone_list = cli .list_hosted_zones_by_name (DNSName = zone_name , MaxItems = '1' )
zone = zone_list ['HostedZones' ][0 ]
if zone ['Name' ] != zone_name :
raise Exception ('zone %s not found!' % zone_name )
zone_id = zone ['Id' ]
waiter = cli .get_waiter ('resource_record_sets_changed' )
waiter .config .delay = 15
waiter .config .max_attempts = 20
def hmac_sha1_hex (k , m ):
return hmac .new (k , m , sha1 ).hexdigest ()
def dkim_pub (rsa ):
return 'v=DKIM1;t=s;p=' + b64e (rsa .publickey ().exportKey ('DER' ))
def dkim_priv (rsa ):
return 'v=DKIM1;t=s;p=;n=e:%s,p:%s,q:%s' % (
long_to_b64 (rsa .e ),
long_to_b64 (rsa .p ),
long_to_b64 (rsa .q ),
)
def date_to_selector (date ):
strdate = date .strftime ('%Y%m%d' )
return strdate + '-' + hmac_sha1_hex (SECRET , strdate )[0 :23 ]
def long_to_b64 (l ):
a = bytearray ()
while l :
a .append (l & 255 )
l >>= 8
a .reverse ()
return b64e (a )
def format_txt (txt ):
# https://aws.amazon.com/premiumsupport/knowledge-center/txtrdatatoolong-error/
return '' .join ([ '"%s"' % txt [i :i + 255 ] for i in xrange (0 , len (txt ), 255 ) ])
def push_record (selector , txt ):
res = cli .change_resource_record_sets (
HostedZoneId = zone_id ,
ChangeBatch = {
'Changes' : [{
'Action' : 'UPSERT' ,
'ResourceRecordSet' : {
'Name' : selector + '.' + zone_name ,
'Type' : 'TXT' ,
'TTL' : 5 ,
'ResourceRecords' : [
{ 'Value' : '"%s"' % txt }
]
}
}]
}
)
print 'waiting on record propagation %s' % res ['ChangeInfo' ]['Id' ]
return waiter .wait (Id = res ['ChangeInfo' ]['Id' ])
def revoke_key (selector ):
#print selector
dkim = 'v=DKIM1;t=s;p='
res = cli .list_resource_record_sets (
HostedZoneId = zone_id ,
StartRecordName = selector + '.' + zone_name ,
StartRecordType = 'TXT' ,
MaxItems = '1'
)
val = res ['ResourceRecordSets' ][0 ]['ResourceRecords' ][0 ]['Value' ]
if val != '"%s"' % dkim :
print selector
print 'push dkim revocation'
push_record (selector , dkim )
def repudiate_key (selector ):
#print selector
filename = '%s_%s.key' % (selector , DOMAIN )
path = BASEDIR + '/' + filename
if not os .path .isfile (path ):
#print 'does not exist'
return
print selector
print 'load private key'
rsa = None
with open (path , 'r' ) as fh :
rsa = RSA .importKey (fh .read ())
print 'generate dkim repudiation'
dkim = dkim_priv (rsa )
print 'push dkim repudiation'
push_record (selector , dkim )
print 'delete repudiated key file'
os .remove (path )
def create_key (selector ):
#print selector
filename = '%s_%s.key' % (selector , DOMAIN )
path = BASEDIR + '/' + filename
if os .path .isfile (path ):
#print 'already exists'
return
print selector
print 'generate rsa key'
rsa = RSA .generate (1024 )
print 'generate dkim record'
dkim = dkim_pub (rsa )
print 'push dkim record'
push_record (selector , dkim )
print 'writing private key to temp file'
tmp_fd , tmp_name = tempfile .mkstemp ('' , '.' + filename + '.' , BASEDIR , True )
os .write (tmp_fd , rsa .exportKey ())
os .fchown (tmp_fd , 0 , grp .getgrnam ('Debian-exim' )[2 ])
os .fchmod (tmp_fd , 0o0440 )
os .fsync (tmp_fd )
os .close (tmp_fd )
print 'renaming temp file'
os .rename (tmp_name , path )
d_start = datetime .utcnow ()
# expire keys
d = d_start
while True :
d = d - timedelta (days = 1 )
filename = '%s_%s.key' % (date_to_selector (d ), DOMAIN )
path = BASEDIR + '/' + filename
if not os .path .isfile (path ):
break
while d < d_start - timedelta (days = 10 ):
repudiate_key (date_to_selector (d ))
d = d + timedelta (days = 1 )
while d < d_start - timedelta (days = 7 ):
revoke_key (date_to_selector (d ))
d = d + timedelta (days = 1 )
#os.exit(1)
# create keys
d = d_start
for _ in xrange (28 ):
create_key (date_to_selector (d ))
d = d + timedelta (days = 1 )