#! /usr/bin/env python3
# (C) Copyright 2021 Hewlett Packard Enterprise Development LP
import sys, subprocess, os.path, argparse, re, socket
if sys.version_info < (3,6):
    exit(sys.argv[0] + ": error: Python version below 3.6 not supported.")

usage='''

SYNTAX
    createcert <SSL_service> -csr [-f] [-keysize <keysize>]
        [<subject_attribute_options>] [<CSR_filename>]

OPTIONS
    -f                        Creates without prompting user
    -csr                      Creates a certificate signing request
    -keysize <keysize>        Specifies encryption key size in bits
    -C  <country>             Specifies country attribute
    -ST <state>               Specifies state attribute
    -L  <locality>            Specifies locality attribute
    -O  <organization>        Specifies organization attribute
    -OU <organizational_unit> Specifies organizational unit attribute
    -CN <common_name>         Specifies common name attribute
    -SAN <subject_alt_name>   Specifies the subject alternative name
    <CSR_filename>            The file the CSR will be written to
    <SSL_service>             Name of SSL service for the certificate

Try "createcert -h" for detailed information for options
 
'''

longhelp='''createcert - Create an SSL certificate signing
    request (CSR) for the quorum witness server

SYNTAX
    createcert <SSL_service> -csr [-f] [-keysize <keysize>]
        [<subject_attribute_options>] [<CSR_filename>]

DESCRIPTION
    The createcert command creates an SSL certificate
    signing request for a specified service.

AUTHORITY
    Super

OPTIONS
    -f
        Used to force operation without prompting the user.

    -csr
        Creates a certificate signing request for the service. No certificates
        are modified and no services are restarted.

    -keysize <keysize>
        Specifies the encryption key size in bits of the self-signed
        certificate. Valid values are 1024 and 2048. The default value
        is 2048.

    -C <country>
        Specifies the value of country (C) attribute of the subject of
        the certificate.

    -ST <state>
        Specifies the value of state (ST) attribute of the subject of
        the certificate.

    -L <locality>
        Specifies the value of locality (L) attribute of the subject of
        the certificate.

    -O <organization>
        Specifies the value of organization (O) attribute of the subject
        of the certificate.

    -OU <organizational_unit>
        Specifies the value of organizational unit (OU) attribute of the
        subject of the certificate.

    -CN <common_name>
        Specifies the value of common name (CN) attribute of the subject
        of the certificate.

    -SAN <subject_alt_name[,subject_alt_name]...>
        Subject alternative name is a X509 extension that allows other
        pieces of information to be associated with the certificate. Multiple
        SANs may delimited with a comma. See Specifiers <subject_alt_name> for
        how to compose SANs.

SPECIFIERS
    <CSR_filename>
       The filename the CSR is written to. This specifier is optional, when the
       <CSR_filename> is not specified the CSR will be printed to the screen.
       This specifier is only valid with the -csr option.

    <subject_alt_name>
        A <subject_alt_name> is composed of <prefix>:<value>. Prefixes are case
        sensitive. These prefixes are supported: DNS, email, IP, RID and URI. These
        prefixes while defined in RFC 5280 are not supported: DirName, EdiPartyName,
        OtherName, and X400Name.

    <SSL_service>
        Valid service names are qw-client and qw-server.

NOTES
    Use the importcert command to import a signed CSR and use showcert
    command to display the certificates.

EXAMPLES
    The following example shows how to create a certificate signing request.
    The encryption key size is 2048 bits.

        $ createcert qw-server -csr -keysize 2048

    The following example show how to create a certificate signing request with
    multiple subject alternative names (SANs).

        $ createcert qw-server -csr -SAN DNS:theresnoplacelikehome.com,IP:127.0.0.1
'''

def exit_usage(msg):
    p.print_usage()
    sys.exit(os.path.basename(sys.argv[0]) + ' error: ' + msg)

prefix='/usr/local/etc/'
qw_server_keyfile=prefix+'key.pem'
qw_server_csr_pem=prefix+'csr.pem'
qw_server_pem=prefix+'cert.pem'
qw_openssl_cnf=prefix+'qwserv/qw.openssl.cnf'

all_services=['qw-client', 'qw-server']

p=argparse.ArgumentParser(usage=usage, add_help=False)
p.add_argument('-h', '--help', action='store_true')
p.add_argument('service', metavar='SSL_service', nargs='?')
p.add_argument('-f', action='store_true')
p.add_argument('-keysize', default='2048', choices=['1024', '2048'])
p.add_argument('-days', default='1095')    # Default is 3 years.
p.add_argument('-C')
p.add_argument('-ST')
p.add_argument('-L')
p.add_argument('-O')
p.add_argument('-OU')
p.add_argument('-CN')
p.add_argument('-SAN')
p.add_argument('filename', metavar='CSR_filename', nargs='?')
g = p.add_mutually_exclusive_group()
g.add_argument('-csr', action='store_true')
g.add_argument('-selfsigned', action='store_true')
args,extra=p.parse_known_args()   # Only parse known arguments in order to leave the optional filename argument in extra.

if args.help:
    print(longhelp)
    sys.exit(0)

if not args.service:
    exit_usage('insufficient arguments')

if not args.filename and len(extra):   # If the filename argument wasn't detected, get it from extra, if present.
    args.filename=extra.pop(0)
    if args.filename[0]=='-':            # Check for filename beginning with '-' character.
        exit_usage('invalid option: '+args.filename)

if len(extra):  exit_usage('extra arguments: '+" ".join(extra))

if not args.service in all_services:
    exit_usage('argument SSL_service: invalid choice: %s (choose from %s)' % (args.service, ", ".join(all_services)) )

if not args.csr and not args.selfsigned:
    exit_usage('Certificate type must be defined; either -csr or -selfsigned must be used.')

if args.selfsigned:
    exit_usage('Selfsigned option is not supported')

if args.csr:
    if args.service!='qw-server':
        exit_usage(args.service+' must be an external certificate authority and cannot be generated on the quorum witness server.')
    x509='-verbose'  # Use a dummy parameter as an argument placeholder in the csr case.

if not args.CN:
    args.CN=socket.getfqdn()
    if not args.f:   # If the -f option is not used, prompt the user to confirm the fqdn as the default CN.
        reply=input('No host name was specified with -CN; Is the hostname %s correct (yes/no)? ' % (args.CN) )
        while True:
            if reply=='yes':   break
            elif reply=='no':  sys.exit(0)
            else:              reply=input('Please type "yes" or "no": ')

if not args.SAN:     # If SAN is not defined, assign it as a copy of the CN.
    args.SAN='DNS:'+args.CN

if args.C and not re.search(r"^[a-zA-Z]{2}$", args.C):
    exit_usage('Country needs to be specified as a two-letter country code.')

if (not args.days.isnumeric()) or int(args.days)<1 or int(args.days)>3650:
    exit_usage('Days must be specified by an integer between 1 and 3650.')

subject=''           # Construct the subject string.
if args.C:   subject+='/C='+args.C
if args.ST:  subject+='/ST='+args.ST
if args.L:   subject+='/L='+args.L
if args.O:   subject+='/O='+args.O
if args.OU:  subject+='/OU='+args.OU
subject+='/CN='+args.CN

p1=subprocess.run(['openssl', 'req', x509, '-newkey', 'rsa:'+args.keysize, '-nodes', '-keyout', qw_server_keyfile,
    '-config', qw_openssl_cnf, '-extensions', 'server_cert',
    '-out', qw_server_csr_pem, '-subj', subject, '-addext', 'subjectAltName = '+args.SAN],
    stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
if p1.returncode and p1.stderr:
    if re.search(r'error in extension(.*?)subjectAltName', p1.stderr):
        exit_usage('unable to create CSR, invalid Subject Alternative Name with value: '+args.SAN)
    exit_usage(p1.stderr)    # If error is not recognized, output the raw error from openssl.

# Read in the new private key.
with open(qw_server_keyfile, 'r') as file:
    new_key=file.read()         # Read in the entire file contents.

old_cert_pem=''
# May need to bring forward some existing certs from the server cert.pem file.
if os.path.isfile(qw_server_pem):
    with open(qw_server_pem, 'r') as file:
        server_pem=file.read()         # Read in the entire file contents.

    # Find the first certificate.
    sr=re.search(r'(?sm)(-----BEGIN CERTIFICATE-----)', server_pem)
    if sr:
        old_cert_pem=server_pem[sr.span(1)[0]:]  # Copy starting with the first found certificate.

 # Reassemble and write the new content to the qw_server_pem file location.
with open(qw_server_pem, 'w+') as file:
    file.write(new_key+'\n')          # Save the new key as the first item in the file.
    file.write(old_cert_pem+'\n')     # Save back the previous certs, if any.

os.unlink(qw_server_keyfile)    # Remove the temporary key file.

if args.filename:     # Save a pem formatted copy of the csr or certificate to the named file.
    p2=subprocess.run(['cp', qw_server_csr_pem, args.filename],
        stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
    if p2.returncode and p2.stderr:
        exit_usage(p2.stderr)
else:
    with open(qw_server_csr_pem, 'r') as file:
        server_pem=file.read()         # Read in the entire file contents.
    print(server_pem)
