HEX
Server: Apache/2.4.63 (Unix)
System: Linux TOMS-220NAS 4.4.302+ #86009 SMP Wed Nov 26 18:19:17 CST 2025 x86_64
User: flavio87 (1026)
PHP: 8.3.27
Disabled: NONE
Upload Files
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)