#!/usr/bin/python3

"""
* File: clkeys
* Version  : 1.3
* License  : BSD-3-Clause
*
* Copyright (c) 2023 - 2026
*	Ralf Senderek, Ireland.  All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
*    notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
*    notice, this list of conditions and the following disclaimer in the
*    documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
*    must display the following acknowledgement:
*	   This product includes software developed by Ralf Senderek.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
"""

import sys, os, re
from binascii import *

ERR_CL         = -1
OK             = 0
ERR_USE        = 1
ERR_PERM       = 2
ERR_PASSWORD   = 3
ERR_INSTALL    = 4
ERR_BITS       = 5
ERR_SIZE       = 7
ERR_CORRUPT    = 10
ERR_INCOMPLETE = 11

Mode              = "generate"
MinPasswordLength = 8
MaxPasswordLength = 64
Version           = "1.3"
KeyFileName       = "./rsakeyfile.p15"
CertFileName      = "./certificate.pem"
CSRFileName       = "./CSR"
NewCertFileName   = "./newcertificate.pem"
status            = 0
RSABits           = 2048
MinRSABits        = 1536
MaxRSABits        = 4096
ALGO              = "RSA"
ImportCert        = bytearray()
KeyFileExists     = False
Name              = bytearray()
KeyFile           = bytearray()
Password          = bytearray()

CertDN       = bytearray()
CertCOUNTRY  = "."
CertSP       = "."
CertLOCALITY = "."
CertORG      = "."
CertOU       = "."
CertNAME     = "."
CertEMAIL    = "."
BINARY       = False
regex        = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b'

try:
     from cryptlib_py import *
except:
     ERR_IMPORT = """
     The python3 library is not installed. You need to install the packages cryptlib-python3 and cryptlib.
     You will find them for a variety of operating systems here:
            https://senderek.ie/cryptlib
     or in the Fedora repository.
     """
     print( ERR_IMPORT )
     exit( ERR_INSTALL )

#-----------------------------------------------------------#
def get_password():
     # writes a bytearray without \n into the global variable Password
     import subprocess
     global Password
     
     Password = bytearray()
     try:
          Result = subprocess.run([ASKPASS], capture_output=True)
          if Result and  Result.returncode == 0 :
               Password = bytearray(Result.stdout.strip())
               # overwrite Result.stdout
               Result.stdout = b'0'*64
     except:
          # recover tty after keyboard interrupt, when echo is turned off
          Result = subprocess.run(["/bin/stty","sane"])

ASKPASS = "/bin/systemd-ask-password"
if not os.path.isfile(ASKPASS) :
     print ("Error: Please install " + ASKPASS + " to ensure safe password input")
     exit(ERR_INSTALL)




#-----------------------------------------------------------#
def sanitize(data):
     forbidden = "!\"§$%()[]{}=?*+~,<>\\"
     good = ""
     for i in range(len(data)) :
          if (data[i] not in forbidden) and (ord(data[i]) < 128):
               good += data[i]
     return good

#-----------------------------------------------------------#
def chmodf (FileName):
     #FileName is a string
     try:
          os.chmod(sanitize(FileName),0o600)
          return True
     except:
          return False

#-----------------------------------------------------------#
def clonef (Src, Dst):
     #Src and Dst are strings
     import shutil
     try:
          shutil.copy2(sanitize(Src), sanitize(Dst))
          return True
     except:
          return False

#-----------------------------------------------------------#
def dumpasn (FileName):
     import subprocess
     List = []
     Result = subprocess.run(["/bin/dumpasn1","-z", sanitize(FileName)], capture_output=True)
     if Result and  Result.returncode == 0 :
          RESULT = Result.stdout.decode()
          List = RESULT.split('\n')
          return List
     else:
          return []

#-----------------------------------------------------------#
def get_random_bytes ( num ):
     # this function does not need to produce cryptographically secure random numbers
     try:
          from random import randbytes
          return randbytes( num )
     except:
          RandomBuffer = bytearray(b' '*num)
          RandomContext_object = cryptCreateContext( cryptUser, CRYPT_ALGO_AES )
          RandomContext = int( RandomContext_object )
          cryptSetAttribute( RandomContext, CRYPT_CTXINFO_MODE, CRYPT_MODE_CFB )
          cryptGenerateKey( RandomContext )
          cryptEncrypt( RandomContext, RandomBuffer )
          cryptDestroyContext( RandomContext )
          return RandomBuffer

#-----------------------------------------------------------#
def clean():
     global privKeyContext
     global cryptKeyset
     try:
          cryptKeysetClose( cryptKeyset )
          if not (Mode == "import"):
               cryptDestroyContext( privKeyContext )
          cryptEnd()
     except:
          cryptEnd()

#-----------------------------------------------------------#
def clean_request():
     global pubKey
     global CertRequest
     cryptDestroyContext( pubKey )
     cryptDestroyContext( CertRequest )
     clean()

#-----------------------------------------------#
def write_bytes(buff, pathname):
     # writes a bytearray into a file

     pathname = sanitize(pathname)
     print("Writing " + str(len(buff)) + " bytes to "  + pathname )
     if len(pathname) == 0:
          print("Error: Illegal file name.")
          return False
     try:
          F = open(pathname,'wb')
          F.write(buff)
          F.close()
          chmodf(pathname)
          return True
     except:
          print (str( sys.exc_info()[0]) )
          print ("Error: cannot write to " + pathname)
          return False

#-----------------------------------------------------------#
def write_CSR(buff, pathname):
     # writes a bytearray into a file
     try:
          F = open(pathname,'w')
          F.write("-----BEGIN CERTIFICATE REQUEST-----\n")
          ASCII = b2a_base64(buff)
          ASCII = ASCII[:-1]
          i = 0
          while i < len(ASCII) :
               line = ASCII[i:i+64]
               i = i + 64
               F.write(line.decode())
               if i < len(ASCII) :
                    F.write("\n")
          F.write("\n")
          F.write("-----END CERTIFICATE REQUEST-----\n")
          chmodf( pathname  )
     except:
          print ("Error: cannot write to " + pathname)

#-----------------------------------------------------------#
def collect_attributes():
     global CertCOUNTRY, CertSP, CertLOCALITY, CertORG, CertOU, CertNAME, CertEMAIL

     intro = """
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name.
There are quite a few fields but you can leave some blank.
If you enter '.', the field will be left blank.

A country name, a common name and an email address are mandatory.
"""
     print (intro)
     CertCOUNTRY  = sanitize( input( "Country Name (2 letter code)       " ))
     CertSP       = sanitize( input( "State or Province Name (full name) " ))
     CertLOCALITY = sanitize( input( "Locality Name (eg, city)           " ))
     CertORG      = sanitize( input( "Organization Name (eg, company)    " ))
     CertOU       = sanitize( input( "Organizational Unit Name           " ))
     CertNAME     = Name.decode() + sanitize( input( "Common Name (enter YOUR name)      " + Name.decode() ))
     CertEMAIL    = sanitize( input( "SAN: Email Address                 " ))

#-----------------------------------------------------------#
def readCertificateFromFile( FileName, Header ):
     global BINARY

     Header = Header + "-----"
     F = open( FileName, "rb" )
     data = F.read()
     F.close()

     start = end = 0
     BEGIN = bytearray(b'-----BEGIN ')
     BEGIN.extend(Header.encode())
     END = bytearray(b'-----END ')
     END.extend(Header.encode())
     begin = False
     Length = len( data )
     ASCII = bytearray()

     i = j = 0
     while ( (not begin) and (i < Length) ) :
          while ((i < Length) and (data[i] != 45)) :
               i = i + 1
          if (i < Length) :
               begin = True
          # hit first -
          j = 0
          while ((j < (len(BEGIN) -1)) and begin and (i < Length)) :
               if (data[i] != BEGIN[j]) :
                   begin = False
               i = i + 1
               j = j + 1
          if (begin) :      
               start = i - len(BEGIN) + 1
     # find -----END CERTIFICATE----- or  -----END CERTIFICATE REQUEST-----
     begin = False
     while ( (not begin) and (i < Length) ) :
          while ((i < Length) and (data[i] != 45)) :
               i = i + 1
          if (i < Length) :
               begin = True
          # hit first -
          j = 0
          while ((j < (len(END) )) and begin and (i < Length)) :
               if (data[i] != END[j]) :
                   begin = False
               i = i + 1
               j = j + 1
          if (begin) :
               end = i 
               # copy start to end to ASCII block
               i = start
               j = 0
               while (i < end) :
                    ASCII.append( data[i] )
                    j = j + 1
                    i = i + 1
     BINARY = False
     if ( (start == 0) and (end == 0) ) :
          # data is probably binary input string
          BINARY = True
          i = start
          j = 0
          while (i < len(data)) :
               ASCII.append( data[i] )
               j = j + 1
               i = i + 1

     return ASCII

#-----------------------------------------------------------#
def get_asn1 (FileName):
     # returns a string
     List = [] 
     try:
          List = dumpasn( FileName )
     except:
          print("Cannot read keyset file " + FileName)
          exit ( ERR_PERM )
     return List

#-----------------------------------------------------------#
def get_DN_from_keyset (FileName):
     # The file must contain an ASN1 structure of a Cryptlib-generated keyset
     # returns String
     DN = ""
     try:
          List = dumpasn( FileName )
          if "pkcs15content" in List[1]:
               DNLine = List[10]
               if "UTF8String" in DNLine:
                    pos = DNLine.find("UTF8String ")
                    DN = DNLine[pos+12:-1]
                    if DN:
                         return DN
     except:
          print("Cannot read keyset file " + FileName)
     return DN

#-----------------------------------------------------------#

if ( len(sys.argv) >= 2 ):
     # legitimate options are in the parameter list

     if  "generate" in sys.argv  :
          Mode = "generate"
          sys.argv.remove( "generate" )
     elif "request" in sys.argv	:  
          Mode = "request"
          sys.argv.remove( "request" )
     elif "import" in sys.argv :
          Mode = "import"
          sys.argv.remove( "import" )
     elif "casign" in sys.argv :
          Mode = "casign"
          sys.argv.remove( "casign" )
     elif "list" in sys.argv :
          Mode = "list"
          sys.argv.remove( "list" )
     elif "-version" in sys.argv :
          print ( Version )
          exit( OK )

     else:
          print("You need to specify an operation: generate or request or import or casign or list")
          exit( ERR_USE )

     if  "-DSA" in sys.argv :
          ALGO = "DSA"
          sys.argv.remove( "-DSA" )
else:
     print( "usage: clkeys generate [-DSA] KeysetName [-SIZE RSABits] [-CN YourName]" )
     print( "       clkeys request KeysetName [-CN YourName]")
     print( "       clkeys import KeysetName CertFile")
     print( "       clkeys casign CAKeysetName RequestFile")
     print( "       clkeys list KeysetName")
     exit(ERR_USE)

# all modes are processed

if Mode == "generate":
     if ALGO == "RSA":
          print ("Generating a RSA key pair.")
     if ALGO == "DSA":
          print ("Generating a DSA key pair.")

if Mode == "request":
     print ("Writing a CertificateSigningRequest for a RSA key pair.")

if Mode == "import":
     print ("Importing a CA-signed certificate from file.")

if Mode == "casign":
     print ("Signing a request with a CA-key and write the user's certificate to a file.")

if len(sys.argv) >= 2 :
     SafeKeysetName = sanitize( sys.argv[1] )
     # allow the Keyset name to end in ".p15" on the command line
     if len( SafeKeysetName) < 2 :
          print("The keyset name is invalid.")
          exit( ERR_USE )

     if SafeKeysetName[-4:] == ".p15" :
          KeyFileName  = SafeKeysetName
          KeyBaseName  = SafeKeysetName[:-4] + ".base.p15"
     else:
          KeyFileName  = SafeKeysetName + ".p15"
          KeyBaseName  = SafeKeysetName + ".base.p15"

     # check if keyset file exists
     if os.path.isfile (KeyFileName):
          KeyFileExists = True
          if Mode == "generate":
               print( "The keyfile " + KeyFileName + " exists. Please rename or remove it." )
               exit( ERR_USE )
          else:
               if os.path.getsize(KeyFileName) < 100 :
                    print( "There is a keyset named " + KeyFileName +" but it is not a valid keyset file." )
                    print( "Please use generate to create a new keyset or use a different name." )
                    exit( ERR_CORRUPT )
     else:
         # request, import, casign and list need an existing keyset
         if (Mode == "request") or (Mode == "import") or (Mode == "casign") or (Mode == "list"):
              print( "There is no keyset named " + KeyFileName )
              exit( ERR_USE )

     if SafeKeysetName[-4:] == ".p15" :
          Name.extend( SafeKeysetName[:-4].encode() )
     else:
          Name.extend( SafeKeysetName.encode() )
     KeyFile.extend( KeyFileName.encode() )

     if Mode == "generate":
          if SafeKeysetName[-4:] == ".p15" :
               CertFileName = SafeKeysetName[:-4] + ".cert.pem"
          else:
               CertFileName = SafeKeysetName + ".cert.pem"
          print( "Storing keys in the file " + KeyFileName + " and " + CertFileName )

     if Mode == "request":
          if SafeKeysetName[-4:] == ".p15" :
               CSRFileName = SafeKeysetName[:-4] + ".CSR"
          else:
               CSRFileName = SafeKeysetName + ".CSR"
          print( "Using existing keys in the keyset file " + KeyFileName + " and storing the new request in " + CSRFileName + ".pem" )
     if Mode == "import":
          print( "Importing a certificate into keyset " + KeyFileName )

     if Mode == "casign":
          print( "Using the keyset " + KeyFileName + " with the CA private key and CA certificate")

     if Mode == "list":
          print ("Listing the ASN1 structure of the keyset " + KeyFileName + ".")

     if (len (sys.argv) >= 3) and ((Mode == "generate") or (Mode == "request")) :
          # check -SIZE
          if ((sys.argv[2] == "-SIZE") or (sys.argv[2] == "-size")) and (Mode == "generate") :
               # read RSA bits
               try:
                    RSABits = int( sys.argv[3] )
                    sys.argv.pop(3)
                    sys.argv.pop(2)
                    print("RSABits is set to : " + str(RSABits))
                    if ( (RSABits < MinRSABits) or (RSABits > MaxRSABits) ) :
                         print( "RSABits must be greater than " + str(MinRSABits) + " but not exceed " + str(MaxRSABits) )
                         exit( ERR_SIZE )
               except ValueError:
                    print( "Illegal RSABits " + str(sys.argv[3]) )
                    exit( ERR_BITS )

          if len(sys.argv) >= 3 and ((sys.argv[2] != "-CN") and (sys.argv[2] != "-cn")) :
               # illegal parameter at pos 2, remove it silently
               sys.argv.pop(2)

          if len(sys.argv) >= 3 and (sys.argv[2] == "-CN" or sys.argv[2] == "-cn"):
	       # overwrite Name with the new Common Name
               if len(sys.argv) > 3 :
                    i = 3
                    Name = bytearray()
                    Name.extend( sanitize(sys.argv[i]).encode() )
                    while i < len(sys.argv)-1 :
                         i += 1 
                         Name.extend( " ".encode() )
                         Name.extend( sanitize(sys.argv[i]).encode() )
          print("Using the KEYID \'" + Name.decode() + "\'")

     if (Mode == "import") or (Mode == "casign") :
          if (len(sys.argv) >= 3) :
               # process a cert file name
               try:
                    # read the certificate file
                    if (Mode == "import") :
                         ImportCert = readCertificateFromFile(sys.argv[2], "CERTIFICATE")
                    if (Mode == "casign") :
                         ImportCert = readCertificateFromFile(sys.argv[2], "CERTIFICATE REQUEST")
                         if (sys.argv[2][-8:] == ".CSR.pem") or (sys.argv[2][-8:] == ".CSR.der") :
                              NewCertFileName = sys.argv[2][:-8] + ".newcert.pem"
               except:
                    print( "Cannot read the certificate or request " + sys.argv[2] + "." )
                    exit( ERR_PERM )
          else:
               # a required file name is missing
               print( "You need to use a file name for the certificate or request." )
               exit( ERR_USE )
else:
     print( "usage: clkeys generate [-DSA] KeysetName [-SIZE RSABits] [-CN YourName]")
     print( "       clkeys request KeysetName [-CN YourName]")
     print( "       clkeys import KeysetName CertFile")
     print( "       clkeys casign CAKeysetName RequestFile")
     print( "       clkeys list KeysetName")
     exit(ERR_USE)


##### Begin Cryptlib code #####
try:
     cryptUser = CRYPT_UNUSED

     cryptInit()

     # collect randomness information
     try:
          cryptAddRandom( CRYPT_RANDOM_SLOWPOLL )
     except CryptException as e :
          status, message = e.args
          print("Warning: RANDOM_SLOWPOLL failed. [" + message + "]")


     # get Cryptlib Version
     Major = cryptGetAttribute(CRYPT_UNUSED, CRYPT_OPTION_INFO_MAJORVERSION)
     Minor = cryptGetAttribute(CRYPT_UNUSED, CRYPT_OPTION_INFO_MINORVERSION)
     Step  = cryptGetAttribute(CRYPT_UNUSED, CRYPT_OPTION_INFO_STEPPING)
     CryptlibVersion = str(Major)+"."+str(Minor)+"."+str(Step)
     print( "clkeys " + Version + " uses Cryptlib version " + CryptlibVersion + "\n")

     if (Mode == "list") :
          ASN1 = get_asn1 (KeyFileName)
          for Line in ASN1:
               print(Line)
          # nothing to clean !
          cryptEnd()
          exit( OK )

     if ( (Mode == "generate") ) :
          # create a new FILE keyset
          cryptKeyset_Object = cryptKeysetOpen( cryptUser, CRYPT_KEYSET_FILE, KeyFile, CRYPT_KEYOPT_CREATE )
          cryptKeyset = int( cryptKeyset_Object )

          # generate a key pair into the keyset
          if ALGO == "RSA" :
               privKeyContext_Object = cryptCreateContext( cryptUser, CRYPT_ALGO_RSA )
               privKeyContext  = int ( privKeyContext_Object )

          if ALGO == "DSA":
               privKeyContext_Object = cryptCreateContext( cryptUser, CRYPT_ALGO_DSA )
               privKeyContext  = int ( privKeyContext_Object )

          cryptSetAttributeString( privKeyContext, CRYPT_CTXINFO_LABEL, Name )
          if ALGO == "RSA" :
               cryptSetAttribute( privKeyContext, CRYPT_CTXINFO_KEYSIZE, int( RSABits/8 ) )
          cryptGenerateKey( privKeyContext )

          # get a password to encrypt the private key
          print("Please enter the password used to protect the private key: ")
          get_password()
          if len( Password ) >= MinPasswordLength and len( Password ) <= MaxPasswordLength :
               cryptAddPrivateKey( cryptKeyset, privKeyContext, Password )
               # randomize password buffer as it is no longer needed
               Password = get_random_bytes( len(Password) )
               del Password
          else:
               print ("Error: Your password must have at least " + str(MinPasswordLength) + " characters. Nothing done.")
               clean()
               exit (ERR_PASSWORD)

          # selfsign a simplified Certificate
          cryptCertificate_Object = cryptCreateCert( cryptUser, CRYPT_CERTTYPE_CERTIFICATE )
          cryptCertificate = int (cryptCertificate_Object)
          cryptSetAttribute( cryptCertificate, CRYPT_CERTINFO_SUBJECTPUBLICKEYINFO, privKeyContext )
          cryptSetAttribute( cryptCertificate, CRYPT_CERTINFO_XYZZY, 1)
          cryptSetAttributeString( cryptCertificate, CRYPT_CERTINFO_COMMONNAME, Name )
          cryptSignCert( cryptCertificate, privKeyContext )

          # do not add the selfsiged certificate, use import to add it to the keyset
          # export the selfsigned certificate

          certFormatType = CRYPT_CERTFORMAT_TEXT_CERTIFICATE
          certMaxLength = cryptExportCert( None, 0, certFormatType, cryptCertificate )
          EncodedCert = bytearray(b' '*certMaxLength)
          print ("\nCertLength: " + str(certMaxLength) + " bytes")
          cryptExportCert( EncodedCert, certMaxLength, certFormatType, cryptCertificate )
          print(EncodedCert.decode())

          try:
               # write cert to file
               write_bytes(EncodedCert, CertFileName)

               # write KEYID to file
               write_bytes(Name, KeyFileName + ".KEYID")
               
	       # the keyset must be closed before it can be copied
               cryptKeysetClose( cryptKeyset )
               cryptDestroyContext( privKeyContext )
               cryptDestroyContext( cryptCertificate )
               chmodf(KeyFileName)
               clonef( KeyFileName , KeyBaseName )
               cryptEnd()
          except:
               print("Cannot write files.")
               cryptDestroyContext( cryptCertificate )
               clean()
               exit( ERR_PERM )
          # exit as Keyset is already closed and all contexts are destroyed
          exit( OK )

     if (Mode == "request") :
          try:
               cryptKeyset_Object = cryptKeysetOpen( cryptUser, CRYPT_KEYSET_FILE, KeyFile, CRYPT_KEYOPT_READONLY )
               cryptKeyset = int( cryptKeyset_Object )
          except CryptException as e :
               status, message = e.args
               if status == CRYPT_ERROR_BADDATA :
                    print( "Error: The keyset file " + KeyFile.decode() + " cannot be used. It may be corrupted." )
               cryptEnd( )
               exit( ERR_CORRUPT )

          # get the public key
          try:
               pubKey_Object = cryptGetPublicKey( cryptKeyset, CRYPT_KEYID_NAME, Name )
               pubKey = int (pubKey_Object)
          except CryptException as e :
               status, message = e.args
               if status == CRYPT_ERROR_NOTFOUND :
                    print( "Error: You are using the wrong KEYID, please use -DN YourName instead." )
                    Owner = get_DN_from_keyset (KeyFile.decode())
                    if Owner :
                         print( "       Found the key-owner name " + Owner + " in the keyset file" )
                    clean()
                    exit( ERR_CORRUPT )

          # get the private key
          print("Please enter the password that you use to protect the private key: ")
          get_password()
          if len( Password ) >= MinPasswordLength and len( Password ) <= MaxPasswordLength :
               try:
                    privKeyContext_Object = cryptGetPrivateKey(cryptKeyset, CRYPT_KEYID_NAME, Name, Password)
                    privKeyContext = int( privKeyContext_Object )
               except CryptException as e :
                    status, message = e.args
                    # randomize password buffer as it is no longer needed
                    Password = get_random_bytes( len(Password) )
                    del Password
                    if status == CRYPT_ERROR_WRONGKEY :
                         print( "Error: You have entered the wrong password. Private key is unavailable." )
                         cryptDestroyContext( pubKey )
                         clean()
                         exit( ERR_PASSWORD )
                    elif status == CRYPT_ERROR_NOTFOUND :
                         print( "Error: You are using the wrong KEYID, please use -DN YourName instead." )
                         Owner = get_DN_from_keyset (KeyFile.decode()) 
                         if Owner :
                              print( "       Found the key-owner name " + Owner + " in the keyset file" )
                         cryptDestroyContext( pubKey )
                         clean()
                         exit( ERR_PASSWORD )

               # randomize password buffer as it is no longer needed
               Password = get_random_bytes( len(Password) )
               del Password
          else:
               print ("Error: Your password must have at least " + str(MinPasswordLength) + " characters. Nothing done.")
               Password = get_random_bytes( len(Password) )
               cryptDestroyContext( pubKey )
               clean()
               exit (ERR_PASSWORD)

	  # create a CertificateSigningRequest
          CertRequest_Object = cryptCreateCert(  cryptUser, CRYPT_CERTTYPE_CERTREQUEST )
          CertRequest = int( CertRequest_Object )
          cryptSetAttribute( CertRequest, CRYPT_CERTINFO_SUBJECTPUBLICKEYINFO, pubKey )

          # add DN info
          collect_attributes()

          if ( not re.fullmatch(regex, CertEMAIL) ) :
               print( "The email address \"" + CertEMAIL + "\" is invalid!" )
               cryptDestroyContext( pubKey )
               cryptDestroyContext( CertRequest )
               clean()
               exit ( ERR_INCOMPLETE )

          if CertNAME == "." or CertEMAIL == "." or len(CertNAME) < 3  or len(CertEMAIL) == 0 :
               print( "You need to enter a common name and a valid email address!" )
               clean_request()
               exit ( ERR_INCOMPLETE )

          if CertCOUNTRY == "." or len(CertCOUNTRY) != 2 :
               print("Please provide a correct country code")
               clean_request()
               exit(ERR_INCOMPLETE)

          # set the minimal required fields
          cryptSetAttributeString( CertRequest, CRYPT_CERTINFO_COUNTRYNAME, bytearray(CertCOUNTRY.encode()) )
          cryptSetAttributeString( CertRequest, CRYPT_CERTINFO_COMMONNAME, bytearray(CertNAME.encode()) )

          # additional information
          if CertOU != "." and CertOU :
               cryptSetAttributeString( CertRequest, CRYPT_CERTINFO_ORGANISATIONALUNITNAME, bytearray(CertOU.encode()) )

          if CertORG != "." and CertORG :
               cryptSetAttributeString( CertRequest, CRYPT_CERTINFO_ORGANISATIONNAME, bytearray(CertORG.encode()) )

          if CertLOCALITY != "." and CertLOCALITY :
               cryptSetAttributeString( CertRequest, CRYPT_CERTINFO_LOCALITYNAME, bytearray(CertLOCALITY.encode()) )

          if CertSP != "." and CertSP :
               cryptSetAttributeString( CertRequest, CRYPT_CERTINFO_STATEORPROVINCENAME, bytearray(CertSP.encode()) )

          try:
               cryptSetAttributeString( CertRequest, CRYPT_CERTINFO_EMAIL, bytearray(CertEMAIL.encode()) )
          except CryptException as e :
               status, message = e.args
               if status == CRYPT_ERROR_PARAM4 :
                    print( "Error: You need to enter a valid email address!" )
                    clean_request()
                    exit( ERR_INCOMPLETE )

          try:
               cryptSignCert( CertRequest, privKeyContext )
          except CryptException as e :
               status, message = e.args
               if status == CRYPT_ERROR_NOTINITED :
                    print( "Error: You need to enter at least a country name, a common name." )
                    clean_request()
                    exit( ERR_INCOMPLETE )

          requestMaxLength = cryptExportCert( None, 0, CRYPT_CERTFORMAT_CERTIFICATE, CertRequest )
          EncodedCSR = bytearray(b' '*requestMaxLength)
          print ("\nGenerating a CSR of length: " + str(requestMaxLength) + " bytes.")
          cryptExportCert( EncodedCSR, requestMaxLength, CRYPT_CERTFORMAT_CERTIFICATE, CertRequest )

          try:
               # write CSR to file 
               print( "Writing the certificate signing request to the file " + CSRFileName+".pem ." )
               write_CSR(EncodedCSR, CSRFileName+".pem")

               # also write the binary content to a .der file
               write_bytes(EncodedCSR, CSRFileName+".der")
          except:
               print("Cannot write files.")
               clean_request()
               exit( ERR_PERM )

     if (Mode == "import") :

          cryptCertificate_Object = cryptImportCert( ImportCert, cryptUser )
          cryptCertificate = int( cryptCertificate_Object )
          try:
               cryptKeyset_Object = cryptKeysetOpen( cryptUser, CRYPT_KEYSET_FILE, KeyFile, CRYPT_KEYOPT_NONE )
               cryptKeyset = int( cryptKeyset_Object )
          except CryptException as e :
               status, message = e.args
               if status == CRYPT_ERROR_BADDATA :
                    print( "Error: The keyset file " + KeyFile.decode() + " cannot be used. It may be corrupted." )
               cryptDestroyContext( cryptCertificate )
               cryptEnd()   
               exit( ERR_CORRUPT )

          # Update the keyset with the certificate
          try:
               cryptAddPublicKey( cryptKeyset, cryptCertificate )
               print ("The certificate has been imported successfully.")
          except CryptException as e :
               status, message = e.args
               if status == CRYPT_ERROR_PARAM2 :
                    print( "Error: there isn’t already a matching private key present in the keyset" )
               elif status == CRYPT_ERROR_DUPLICATE :
                    print( "This certificate is already imported in the keyset " + KeyFileName )
               else:
                    print( "Error ... " + message )

     if (Mode == "casign") :
          print ("Processing " + str(len(ImportCert)) + " bytes." )
          # turn the request into a certificate object

          try:
               cryptCertRequest_Object = cryptImportCert( ImportCert, cryptUser )
               CertRequest = int( cryptCertRequest_Object )
          except CryptException as e :
               status, message = e.args
               if status == CRYPT_ERROR_BADDATA :
                    print( "Error: The request is probably corrupt." )
                    cryptEnd()
                    exit( ERR_CORRUPT )
          try: 
               cryptCheckCert( CertRequest, cryptUser )
          except CryptException as e :
               status, message = e.args
               if status == CRYPT_ERROR_BADDATA :
                    print( "Error: The request is probably corrupt." )
                    cryptDestroyContext( CertRequest )
                    cryptEnd()
                    exit( ERR_CORRUPT )

          # make sure, that the correct KEYID is being used
          KeyIDFileName = KeyFile.decode() + ".KEYID"
          if  os.path.isfile(KeyIDFileName) :
               KeyIDContent = bytearray()
               F = open (KeyIDFileName, "rb")
               KeyIDContent = F.readlines()
               if len(KeyIDContent) != 0:
                    Name = KeyIDContent[0].strip()
                    print( "Using the key ID \'" + Name.decode() + "\' from file " + KeyIDFileName )
               F.close()

          cryptCertificate_Object = cryptCreateCert( cryptUser, CRYPT_CERTTYPE_CERTIFICATE )
          cryptCertificate = int( cryptCertificate_Object )
          cryptSetAttribute( cryptCertificate,  CRYPT_CERTINFO_CERTREQUEST, CertRequest )

          try:
               cryptKeyset_Object = cryptKeysetOpen( cryptUser, CRYPT_KEYSET_FILE, KeyFile, CRYPT_KEYOPT_READONLY )
               cryptKeyset = int( cryptKeyset_Object )
          except CryptException as e :
               status, message = e.args 
               if status == CRYPT_ERROR_BADDATA :
                    print( "Error: The keyset file " + KeyFile.decode() + " cannot be used. It may be corrupted." )
               
               cryptDestroyContext( CertRequest )
               cryptDestroyContext( cryptCertificate )
               cryptEnd()
               exit( ERR_CORRUPT )

	  # read the CA signing key from the keyset
          print("Please enter the password that you use to protect the private key: ")
          get_password()
          if len( Password ) >= MinPasswordLength and len( Password ) <= MaxPasswordLength :
               try:
                    privKeyContext_Object = cryptGetPrivateKey(cryptKeyset, CRYPT_KEYID_NAME, Name, Password)
                    privKeyContext = int( privKeyContext_Object )
               except CryptException as e :
                    status, message = e.args
                    # randomize password buffer as it is no longer needed
                    Password = get_random_bytes( len(Password) )
                    del Password
                    if status == CRYPT_ERROR_WRONGKEY :
                         print( "Error: You have entered the wrong password. The CA private key is unavailable." )
                         cryptDestroyContext( CertRequest )
                         cryptDestroyContext( cryptCertificate )
                         clean()
                         exit( ERR_PASSWORD )

                    elif status == CRYPT_ERROR_NOTFOUND :
                         print( "Error: The KEYID " + Name.decode() + " is incorrect! The CA private key is unavailable." )
                         Owner = get_DN_from_keyset (KeyFile.decode())
                         if Owner :
                              print( "       Found the CA key-owner name " + Owner + " in the keyset file" )
                              print( "       Please make sure that a proper KEYID file is present for this CA key." )
                         cryptDestroyContext( CertRequest )
                         cryptDestroyContext( cryptCertificate )
                         clean()
                         exit( ERR_USE )

               # randomize password buffer as it is no longer needed
               Password = get_random_bytes( len(Password) )
               del Password
          else:
               print ("Error: Your password must have at least " + str(MinPasswordLength) + " characters. Nothing done.")
               Password = get_random_bytes( len(Password) )
               cryptDestroyContext( CertRequest )
               cryptDestroyContext( cryptCertificate )
               clean()
               exit (ERR_PASSWORD)

          # print the DN to be signed ...
          print()
          DNList = [CRYPT_CERTINFO_COMMONNAME, CRYPT_CERTINFO_EMAIL, CRYPT_CERTINFO_COUNTRYNAME, CRYPT_CERTINFO_STATEORPROVINCENAME, CRYPT_CERTINFO_LOCALITYNAME, CRYPT_CERTINFO_ORGANISATIONNAME, CRYPT_CERTINFO_ORGANISATIONALUNITNAME]
          DNLabel = ["CN = ", "emailAddress = ", "C  = ", "SP = ", "L  = ",  "O  = ", "OU = "]
          i = 0
          while i < len(DNList) :
               try:
                    Info = bytearray(b'                                                ')
                    cryptGetAttributeString( cryptCertificate, DNList[i], Info )
                    print( DNLabel[i] + Info.decode() )
               except:
                    print( DNLabel[i] )
               i = i + 1
          print()

          # ask for confirmation
          REPLY = input("SIGN THIS KEY ? : ")

          if REPLY == "yes" or REPLY == "y" :
               # now sign the certificate
               try:
                    cryptSignCert( cryptCertificate, privKeyContext )
               except CryptException as e :
                    status, message = e.args
                    if status == CRYPT_ERROR_PARAM2 :
                         print( "Error: The keyset file " + KeyFile.decode() + " may not contain the selfsigend certificate." )
                         print( "       Please make sure that the CA certificate is imported! " )
                    cryptDestroyContext( privKeyContext )
                    cryptDestroyContext( cryptCertificate )
                    cryptDestroyContext( CertRequest )
                    clean()
                    exit( ERR_USE )


               # export the new certificate
               CertMaxLength = cryptExportCert( None, 0, CRYPT_CERTFORMAT_CERTIFICATE, cryptCertificate )
               EncodedCert = bytearray(b' '*CertMaxLength)
               print ("\nGenerating a Certificate of length: " + str(CertMaxLength) + " bytes.")
               cryptExportCert( EncodedCert, CertMaxLength, CRYPT_CERTFORMAT_CERTIFICATE, cryptCertificate )

               # write the certificate to the file "newcertificate.pem"
               certFormatType = CRYPT_CERTFORMAT_TEXT_CERTIFICATE
               certMaxLength = cryptExportCert( None, 0, certFormatType, cryptCertificate)
               EncodedCert = bytearray(b' '*certMaxLength)
               print ("\nText Length: " + str(certMaxLength) + " bytes:\n")
               cryptExportCert( EncodedCert, certMaxLength, certFormatType, cryptCertificate )

               print(EncodedCert.decode())

               try:
                    # write cert into a file 
                    write_bytes(EncodedCert, NewCertFileName)
               except:
                    print("Cannot write file " + NewCertFileName)
                    cryptDestroyContext( privKeyContext )
                    cryptDestroyContext( cryptCertificate )
                    cryptDestroyContext( CertRequest )
                    clean()
                    exit( ERR_PERM )
          else:
               print("Nothing done.")


     # destroy all contexts
     if not (Mode == "request") :
          cryptDestroyContext( cryptCertificate )

     if (Mode == "request") :
          cryptDestroyContext( pubKey )

     if (Mode == "casign") or (Mode == "request"):
          cryptDestroyContext( CertRequest )

     # normal clean up
     # destroy cryptKeyset and privKeyContext
     # followed by cryptEnd()
     clean()

except CryptException as e :
     status, message = e.args
     print( "Error ... " + message )
     clean()

exit( OK )
