File: //usr/syno/bin/synokmip_cert
#!/usr/bin/python3
import sys
import argparse
import json
import shutil
import random, string
import time
import fcntl
from os.path import exists
from os import listdir, remove
from os import remove as os_remove
from pathlib import Path
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.x509.oid import NameOID
# constant value
SZF_CERTINFO = "/etc/pykmip/CERTINFO"
SZD_GRANTED_CERTS = "/etc/pykmip/granted_certs/"
SZD_REVOKED_CERTS = "/etc/pykmip/revoked_certs/"
SZD_IMPORTED_CA_CERTS = "/etc/pykmip/imported_ca_certs/"
SZF_SERVER_CA = "/etc/pykmip/imported_ca_certs/server.ca.pem"
SZF_SYNOKMIP_CERT_LOCK = "/run/pykmip/synokmip_cert.lock"
SZF_IMPORTING_CERT_INFO = "/run/pykmip/importing_cert_info"
lock_file = None;
def parse_action_argument(input_arg):
parser = argparse.ArgumentParser(description='Synology KMIP Certificate Tool')
parser.add_argument(
'--import-cert',
dest='import_cert',
action='store_true',
help='import certificate'
)
parser.add_argument(
'--import-server-ca',
dest='import_server_ca',
action='store_true',
help='import server CA'
)
parser.add_argument(
'--renew-cert',
dest='renew_cert',
action='store_true',
help='renew certificate'
)
parser.add_argument(
'--refresh',
dest='refresh',
action='store_true',
help='refresh cert info'
)
parser.add_argument(
'--grant',
dest='grant',
action='store_true',
help='grant certificate'
)
parser.add_argument(
'--revoke',
dest='revoke',
action='store_true',
help='revoke certificate'
)
parser.add_argument(
'--remove',
dest='remove',
action='store_true',
help='remove certificate'
)
parser.add_argument(
'--modify-desc',
dest='modify_desc',
action='store_true',
help='modify description'
)
opts, left = parser.parse_known_args(input_arg)
sys.argv = sys.argv[:1]+left
return opts
def parse_secondary_argument(input_arg, action):
parser = argparse.ArgumentParser(description='Synology KMIP Certificate Tool')
if (action.import_cert):
parser.add_argument(
'--in',
dest='infile',
type=str,
help='input file'
)
parser.add_argument(
'--CA',
dest='CA',
type=str,
help='CA file (optional)'
)
parser.add_argument(
'--desc',
dest='desc',
type=str,
help='certificate desc (optional)'
)
parser.add_argument(
'--granted',
dest='granted',
action='store_true',
help='certificate granted (optional)'
)
if (action.renew_cert):
parser.add_argument(
'--in',
dest='infile',
type=str,
help='input file'
)
parser.add_argument(
'--CA',
dest='CA',
type=str,
help='CA file (optional)'
)
parser.add_argument(
'--id',
dest='id',
type=str,
help='certificate id'
)
if (action.import_server_ca):
parser.add_argument(
'--in',
dest='infile',
type=str,
help='input file'
)
if (action.grant or
action.revoke or
action.remove):
parser.add_argument(
'--id',
dest='id',
type=str,
help='certificate id'
)
if (action.modify_desc):
parser.add_argument(
'--id',
dest='id',
type=str,
help='certificate id'
)
parser.add_argument(
'--desc',
dest='desc',
type=str,
help='certificate desc (optional)'
)
if (len(input_arg) == 0 and
not action.refresh):
input_arg = ['-h']
opts, left = parser.parse_known_args(input_arg)
sys.argv = sys.argv[:1]+left
return opts
def read_pem(file_path):
try:
with open(file_path, 'r') as f:
file_content = f.read()
cert_obj = x509.load_pem_x509_certificate(str.encode(file_content), default_backend())
except Exception:
raise Exception("Load certificate failed.")
return cert_obj
def read_info_json():
if exists(SZF_CERTINFO):
try:
with open(SZF_CERTINFO, 'r') as f:
cert_info = json.load(f)
except Exception:
raise Exception("Load CERTINFO failed.")
return cert_info
else:
return {}
def write_info_json(cert_info):
try:
with open(SZF_CERTINFO, 'w') as f:
json.dump(cert_info, f)
except Exception:
raise Exception("Write CERTINFO failed.")
def dump_importing_cert(cert_info):
try:
with open(SZF_IMPORTING_CERT_INFO, 'w') as f:
json.dump(cert_info, f)
except Exception:
raise Exception("Write {0} failed.".format(SZF_IMPORTING_CERT_INFO))
def dump_cert_obj_to_dict(cert_obj):
retDict = {'cn' : cert_obj.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value,
'issuer' : cert_obj.issuer.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value,
'not_valid_before' : cert_obj.not_valid_before.isoformat(' '),
'not_valid_after' : cert_obj.not_valid_after.isoformat(' ')
}
try:
retDict['subject_alt'] = cert_obj.extensions.get_extension_for_class(x509.SubjectAlternativeName).value.get_values_for_type(x509.DNSName),
except Exception:
retDict['subject_alt'] = ""
return retDict
def creat_cert_dict(cert_obj, desc="", granted=False, ca_obj=None, cert_id=None):
# cert dict format
# {'id' : "cert id"
# 'cn' : "common name",
# 'issuer' : "issuer common name",
# 'subject_alt' : "subject altinative name",
# 'not_valid_before' : "not valid before (datetime)",
# 'not_valid_after' : "not valid after (datetime)",
# 'desc' : "description",
# 'granted' : true/false,
# 'has_import_ca' : true/false
# }
retDict = dump_cert_obj_to_dict(cert_obj)
retDict['desc'] = desc
retDict['id'] = cert_id
if granted:
retDict['granted'] = True
else:
retDict['granted'] = False
if ca_obj is not None:
retDict['has_import_ca'] = True
else:
retDict['has_import_ca'] = False
return retDict
def import_cert(arg):
# check certificate and CA are valid
cert_obj = read_pem(arg.infile)
if (isinstance(arg.CA, str)):
CA_obj = read_pem(arg.CA)
else:
CA_obj = None
# copy to pykmip folder
if arg.granted:
cert_dest = SZD_GRANTED_CERTS
else:
cert_dest = SZD_REVOKED_CERTS
random_id = ''.join(random.choice(string.ascii_letters + string.digits) for x in range(10))
cert_dest += random_id + ".pem"
shutil.copyfile(arg.infile, cert_dest)
if (isinstance(arg.CA, str)):
CA_dest = SZD_IMPORTED_CA_CERTS
CA_dest += random_id + ".ca.pem"
shutil.copyfile(arg.CA, CA_dest)
cert_info_dict = read_info_json()
new_cert_dict = creat_cert_dict(cert_obj, arg.desc, arg.granted, CA_obj, random_id)
dump_importing_cert(new_cert_dict)
cert_info_dict[random_id] = new_cert_dict
write_info_json(cert_info_dict)
def renew_cert(arg):
cert_info_dict = read_info_json()
# check cert is in cert_info
if arg.id not in cert_info_dict:
raise Exception("Cert with {0} not found.".format(arg.id))
# check certificate and CA are valid
cert_obj = read_pem(arg.infile)
if isinstance(arg.CA, str):
CA_obj = read_pem(arg.CA)
else:
CA_obj = None
# copy to pykmip folder
if cert_info_dict[arg.id]["granted"]:
cert_dest = SZD_GRANTED_CERTS
else:
cert_dest = SZD_REVOKED_CERTS
cert_dest += arg.id + ".pem"
os_remove(cert_dest)
shutil.copyfile(arg.infile, cert_dest)
if cert_info_dict[arg.id]["has_import_ca"]:
CA_dest = SZD_IMPORTED_CA_CERTS + arg.id + ".ca.pem"
os_remove(CA_dest)
shutil.copyfile(arg.CA, CA_dest)
else:
CA_obj = None
new_cert_dict = creat_cert_dict(cert_obj, cert_info_dict[arg.id]["desc"], cert_info_dict[arg.id]["granted"], CA_obj, arg.id)
cert_info_dict[arg.id] = new_cert_dict
write_info_json(cert_info_dict)
def import_server_ca(arg):
# check valid
try:
read_pem(arg.infile)
except Exception:
raise Exception("Load server CA failed.")
# copy to pykmip folder
shutil.copyfile(arg.infile, SZF_SERVER_CA)
def refresh_cert_info():
cert_info_dict = read_info_json()
new_cert_info_dict = {}
granted_certs = []
revoked_certs = []
files_in_granted = listdir(SZD_GRANTED_CERTS)
for file in files_in_granted:
if file.endswith(".pem"):
granted_certs.append(file.split('.')[0])
files_in_revoked = listdir(SZD_REVOKED_CERTS)
for file in files_in_revoked:
if file.endswith(".pem"):
revoked_certs.append(file.split('.')[0])
for cert in granted_certs + revoked_certs:
if cert in granted_certs:
new_cert_obj = read_pem(SZD_GRANTED_CERTS + cert + ".pem")
else:
new_cert_obj = read_pem(SZD_REVOKED_CERTS + cert + ".pem")
# try read CA to check exist or not
try:
ca_obj = read_pem(SZD_IMPORTED_CA_CERTS + cert + ".ca.pem")
except Exception:
ca_obj = None
if cert in granted_certs:
new_cert_dict = creat_cert_dict(new_cert_obj, "", True, ca_obj, cert)
else:
new_cert_dict = creat_cert_dict(new_cert_obj, "", False, ca_obj, cert)
if cert in cert_info_dict:
new_cert_dict['desc'] = cert_info_dict[cert]['desc']
new_cert_info_dict[cert] = new_cert_dict
write_info_json(new_cert_info_dict)
def grant(arg):
cert_info_dict = read_info_json()
if arg.id in cert_info_dict:
if cert_info_dict[arg.id]['granted'] is not True:
cert_info_dict[arg.id]['granted'] = True
move_src = SZD_REVOKED_CERTS + arg.id + ".pem"
move_dest = SZD_GRANTED_CERTS + arg.id + ".pem"
shutil.move(move_src, move_dest)
else:
print("{0} is already granted".format(arg.id))
return
else:
raise Exception("Certificate not found.")
write_info_json(cert_info_dict)
def revoke(arg):
cert_info_dict = read_info_json()
if arg.id in cert_info_dict:
if cert_info_dict[arg.id]['granted'] is not False:
cert_info_dict[arg.id]['granted'] = False
move_src = SZD_GRANTED_CERTS + arg.id + ".pem"
move_dest = SZD_REVOKED_CERTS + arg.id + ".pem"
shutil.move(move_src, move_dest)
else:
print("{0} is already revoked".format(arg.id))
return
else:
raise Exception("Certificate not found.")
write_info_json(cert_info_dict)
def remove(arg):
cert_info_dict = read_info_json()
if arg.id in cert_info_dict:
if cert_info_dict[arg.id]['granted'] is True:
cert_file = SZD_GRANTED_CERTS + arg.id + ".pem"
else:
cert_file = SZD_REVOKED_CERTS + arg.id + ".pem"
os_remove(cert_file)
if cert_info_dict[arg.id]['has_import_ca'] is True:
ca_file = SZD_IMPORTED_CA_CERTS + arg.id + ".ca.pem"
os_remove(ca_file)
del cert_info_dict[arg.id]
else:
raise Exception("Certificate not found.")
write_info_json(cert_info_dict)
def modify_desc(arg):
cert_info_dict = read_info_json()
if arg.id in cert_info_dict:
cert_info_dict[arg.id]["desc"] = arg.desc
else:
raise Exception("Certificate not found.")
write_info_json(cert_info_dict)
def acquire_lock():
wait_count = 0
locked = True
if not exists(SZF_SYNOKMIP_CERT_LOCK):
Path(SZF_SYNOKMIP_CERT_LOCK).touch()
lock_file = open(SZF_SYNOKMIP_CERT_LOCK, 'w')
while locked:
try:
fcntl.lockf(lock_file, fcntl.LOCK_EX|fcntl.LOCK_NB)
locked = False
except IOError:
if wait_count > 100:
raise Exception("Get lock timeout.")
time.sleep(0.1)
wait_count += 1
return True
def release_lock():
if lock_file is not None:
fcntl.lockf(lock_file, fcntl.LOCK_UN)
lock_file.close()
def main():
Path("/run/pykmip").mkdir(parents=True, exist_ok=True)
Path(SZD_GRANTED_CERTS).mkdir(parents=True, exist_ok=True)
Path(SZD_REVOKED_CERTS).mkdir(parents=True, exist_ok=True)
Path(SZD_IMPORTED_CA_CERTS).mkdir(parents=True, exist_ok=True)
action_arg = parse_action_argument(sys.argv[1:])
arg = parse_secondary_argument(sys.argv[1:], action_arg)
acquire_lock()
if action_arg.import_cert:
import_cert(arg)
elif action_arg.import_server_ca:
import_server_ca(arg)
elif action_arg.renew_cert:
renew_cert(arg)
elif action_arg.refresh:
refresh_cert_info()
elif action_arg.grant:
grant(arg)
elif action_arg.revoke:
revoke(arg)
elif action_arg.remove:
remove(arg)
elif action_arg.modify_desc:
modify_desc(arg)
release_lock()
if __name__ == '__main__':
main()
sys.exit(0)