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

usage='''

SYNTAX
    The syntax for the showcert command can be one of the following:

    showcert [-showcols <column>,[<column>...]]
        [-service <SSL_service_name(s)>] [-type <certificate_type(s)>]
    showcert {-pem|-text} [-service <SSL_service_name(s)>]
        [-type <certificate_type(s)>] [-file <filename>]
    showcert -listcols

OPTIONS
    -listcols                           Lists valid table columns
    -showcols <column>,[<column>...]    Customizes the table columns
    -service  <SSL_service_name>        The SSL services of interest
    -type     <type>                    The certificate type of interest
    -pem                                Displays the certificates in PEM format
    -text                               Displays the certificates in human
                                            readable format
    -file     <filename>                Sends the output to a file

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

longhelp='''showcert - Show information about SSL certificates of the quorum witness server.

SYNTAX
    The syntax for the showcert command can be one of the following:

    showcert [-showcols <column>,[<column>...]]
        [-service <SSL_service_name(s)>] [-type <certificate_type(s)>]
    showcert {-pem|-text} [-service <SSL_service_name(s)>]
        [-type <certificate_type(s)>] [-file <filename>]
    showcert -listcols

DESCRIPTION
    The showcert command has two forms. The first is a table with a high level
    overview of the certificates used by the SSL Services. This table is
    customizable with the -showcols option. The second form provides detailed
    certificate information in either human readable format or in PEM (Privacy
    Enhanced Mail) format. It can also save the certificates in a specified
    file.

    With both forms the user is able to select the certificates with the
    -service and -type options.

AUTHORITY
    Any role in the system

OPTIONS
    -listcols
        Displays the valid table columns.

    -showcols <column>,[<column>...]
        Changes the columns displayed in the table.

    -service <SSL_service_name(s)>
        Displays only the certificates used by the service(s).
        Multiple services must be delimited by a comma.
        Valid service names are qw-client and qw-server.

    -type <certificate_type(s)>
        Displays only certificates of the specified type, e.g.,
        only root CA. Multiple types must be delimited by a comma.
        Valid types are csr, cert, intca, and rootca.

    -pem
        Displays the certificates in PEM format. When a filename is specified
        the certificates are exported to the file.

    -text
        Displays the certificates in human readable format. When a filename
        is specified the certificates are exported to the file.

    -file <filename>
        Specifies the export file of the -pem or -text option.

SPECIFIERS
    None.

NOTES
    Use the createcert command to create self-signed certificates and
    importcert to import signed certificates.

EXAMPLES
    The following example shows how to display the certificate in table format.

        % showcert

    The following example shows how to display the certificate used by the qw-server
    service in PEM format.

        % showcert -service qw-server -type cert -pem
'''

def exit_usage(msg):
    p.print_usage()
    exit_err(msg)

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

qw_server_csr_pem='/usr/local/etc/csr.pem'
qw_server_pem='/usr/local/etc/cert.pem'
qw_client_pem='/usr/local/etc/cacert.pem'

all_services=['qw-client', 'qw-server']
all_types=['csr', 'cert', 'intca', 'rootca']
all_cols=['commonname', 'enddate', 'fingerprint', 'issuer', 'serial', 'service', 'signaturetype', 'startdate', 'subject', 'subjectaltname', 'type']
default_cols=['service', 'commonname', 'type', 'enddate', 'fingerprint']

p=argparse.ArgumentParser(usage=usage, add_help=False)
p.add_argument('-h', '--help', action='store_true')
p.add_argument('-service')
p.add_argument('-type')
p.add_argument('-file')
g = p.add_mutually_exclusive_group()
g.add_argument('-pem', action='store_true')
g.add_argument('-text', action='store_true')
g.add_argument('-listcols', action='store_true')
g.add_argument('-showcols')
args=p.parse_args()

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

if args.listcols:
    if args.file:
        exit_usage('-file option must only be used with -pem or -text option')
    if args.service:
        exit_usage('-service option not supported with -listcols option')
    if args.type:
        exit_usage('-type option not supported with -listcols option')
    print(",".join(all_cols))
    exit

if not args.service:     # If not set, then set default to all SSL services.
    args.service=",".join(all_services)
services=args.service.split(',')
bad_choices = list(set(services) - set(all_services))  # Find invalid service names.
if bad_choices:
    exit_usage('argument -service: invalid choice: %s (choose from %s)' % (bad_choices[0], ", ".join(all_services)) )

if not args.type:        # If not set, then set default to all certificate types.
    args.type=",".join(all_types)
types=args.type.split(',')
bad_choices = list(set(types) - set(all_types))        # Find invalid certificate types.
if bad_choices:
    exit_usage('argument -type: invalid choice: %s (choose from %s)' % (bad_choices[0], ", ".join(all_types)) )

if not args.showcols and not args.pem and not args.text:    # If not set, then set to default column list.
    args.showcols=",".join(default_cols)
cols=None
if args.showcols:        # If showcols is not empty, this must be a showcols command.
    if args.file:
        exit_usage('-file option must only be used with -pem or -text option')
    cols=args.showcols.split(',')
    bad_choices = list(set(cols) - set(all_cols))          # Find invalid column names.
    if bad_choices:
        exit_usage('argument -showcols: invalid column name(s): %s' % (", ".join(bad_choices)) )

col_widths={}
for i in all_cols: col_widths[i]=len(i)   # Initialize the dictionary of column widths.

handle=None
if args.pem or args.text:     # Set up the correct file handle for output.
    handle = open(args.file, 'w') if args.file else sys.stdout

table=[]   # Declare the output table.
for s_item in services:
    new_row={}
    if s_item=='qw-client':
        pem_file=qw_client_pem
    if s_item=='qw-server':
        pem_file=qw_server_pem
        # Special case, handle csr if qw-server output is requested.
        if os.path.isfile(qw_server_csr_pem) and os.path.getsize(qw_server_csr_pem) and ('csr' in types):
            p1=subprocess.run(['openssl', 'req', '-outform', 'pem', '-in', qw_server_csr_pem],
                stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
            p2=subprocess.run(['openssl', 'req', '-noout', '-text', '-in', qw_server_csr_pem],
                stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
            if p2.returncode and p2.stderr:  exit_err(p2.stderr)
            # This process outputs the csr in der format in order to get the fingerprint in the next step.
            p3=subprocess.Popen(['openssl', 'req', '-outform', 'der', '-in', qw_server_csr_pem],
                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            # This process uses openssl to get the fingerprint for the csr.
            p4=subprocess.run(['openssl', 'dgst', '-sha256',], stdin=p3.stdout,
                stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)

            # Populate various columns with data from the certificate.
            sr=re.search(r"Subject: .*?\bCN\s*=\s*([^,\n]*)", p2.stdout)
            if sr:
                col_commonname=sr.group(1)
            else:
                col_commonname='--'
            if cols and 'commonname' in cols:
                new_row['commonname']=col_commonname
                col_widths['commonname']=max(col_widths['commonname'], len(col_commonname))

            if cols and 'enddate' in cols:
                new_row['enddate']='--'
                col_widths['enddate']=max(col_widths['enddate'], len('--'))

            sr=p4.stdout.split(" ")[1][:40]
            if cols and 'fingerprint' in cols:
                new_row['fingerprint']=sr
                col_widths['fingerprint']=max(col_widths['fingerprint'], len(sr))

            if cols and 'issuer' in cols:
                new_row['issuer']='--'
                col_widths['issuer']=max(col_widths['issuer'], len('--'))

            if cols and 'service' in cols:
                new_row['service']=s_item
                col_widths['service']=max(col_widths['service'], len(s_item))

            if cols and 'startdate' in cols:
                new_row['startdate']='--'
                col_widths['startdate']=max(col_widths['startdate'], len('--'))

            srs=re.search(r"Subject: (.*)", p2.stdout).group(1)
            if cols and 'subject' in cols:
                new_row['subject']=srs
                col_widths['subject']=max(col_widths['subject'], len(srs))

            if cols and 'type' in cols:
                new_row['type']='csr'
                col_widths['type']=max(col_widths['type'], len('csr'))

            if args.pem:        # This is the -pem case where we output certificates in the pem format.
                content=p1.stdout
            elif args.text:     # This is the -text case where we output a human readable x509 certificate.
                content=p2.stdout
            if args.pem or args.text:
                handle.write("Service:%s Type:csr Commonname:%s\n" % (s_item, col_commonname))
                handle.write(content+'\n')
            else:               # This is the -showcols case where we output a table of certificate.
                if len(new_row):  # If the new row contains columns, add it to the table.
                    table.append(new_row)
                    new_row={}
    n=1;
    while n>0:    # Iterate over each certificate in the certificate bundle.
        # First process extracts a single certificate from a potential bundle of certificates in the pem file.
        p1=subprocess.run(['perl', '-ne', "$marker++ if /^-----BEGIN CERT/; print if $marker == %s;" % (n), pem_file],
            stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
        if p1.stdout=='':   # Exit the while-loop if there are no more lines to process.
            break
        pipe_r,pipe_w=os.pipe()   # Pipe for copying data to stdin.
        pipe=os.fdopen(pipe_w, 'w')
        pipe.write(p1.stdout)
        pipe.close()
        # This process uses openssl to format the certificate in human readable format.
        p2=subprocess.run(['openssl', 'x509', '-fingerprint', '-sha256', '-noout', '-text'], stdin=pipe_r,
            stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)

        # Populate various columns with data from the certificate.
        sr=re.search(r"Subject: .*?\bCN\s*=\s*([^,\n]*)", p2.stdout).group(1)
        col_commonname=sr
        if cols and 'commonname' in cols:
            new_row['commonname']=sr
            col_widths['commonname']=max(col_widths['commonname'], len(sr))

        sr=re.search(r"Not After : (.*)", p2.stdout).group(1)
        if cols and 'enddate' in cols:
            new_row['enddate']=sr
            col_widths['enddate']=max(col_widths['enddate'], len(sr))

        sr=re.search(r"Fingerprint=(.*)", p2.stdout).group(1).replace(':', '').lower()[:40]
        if cols and 'fingerprint' in cols:
            new_row['fingerprint']=sr
            col_widths['fingerprint']=max(col_widths['fingerprint'], len(sr))

        sri=re.search(r"Issuer: (.*)", p2.stdout).group(1)
        if cols and 'issuer' in cols:
            new_row['issuer']=sri
            col_widths['issuer']=max(col_widths['issuer'], len(sri))

        if cols and 'service' in cols:
            new_row['service']=s_item
            col_widths['service']=max(col_widths['service'], len(s_item))

        sr=re.search(r"Not Before: (.*)", p2.stdout).group(1)
        if cols and 'startdate' in cols:
            new_row['startdate']=sr
            col_widths['startdate']=max(col_widths['startdate'], len(sr))

        srs=re.search(r"Subject: (.*)", p2.stdout).group(1)
        if cols and 'subject' in cols:
            new_row['subject']=srs
            col_widths['subject']=max(col_widths['subject'], len(srs))

        sr=re.search(r"CA:(TRUE|FALSE)", p2.stdout)
        if not sr or sr.group(1)=='FALSE':   # If there is no CA attribute, or it is FALSE, then type is cert.
            col_type='cert'
        else:
            if sri==srs:
                col_type='rootca'
            else:
                col_type='intca'
        if cols and 'type' in cols:
            new_row['type']=col_type
            col_widths['type']=max(col_widths['type'], len(col_type))

        if col_type in types:
            if args.pem:        # This is the -pem case where we output certificates in the pem format.
                content=p1.stdout
            elif args.text:     # This is the -text case where we output a human readable x509 certificate.
                content=p2.stdout[p2.stdout.find('\n')+1:]   # Skip first line which contains the fingerprint.
            if args.pem or args.text:
                handle.write("Service:%s Type:%s Commonname:%s\n" % (s_item, col_type, col_commonname))
                handle.write(content+'\n')
            else:               # This is the -showcols case where we output a table of certificate.
                if len(new_row):  # If the new row contains columns, add it to the table.
                    table.append(new_row)
                    new_row={}
        n+=1

if handle and (handle is not sys.stdout):   # Close the output file if it is not stdout.
    handle.close()

# Output a table of information about each certificate.
if len(table):
    for c_item in cols:    # Output the column headings for the showcols table.
        print(c_item.ljust(col_widths[c_item]+1).capitalize(), end='')
    print('')
    for t_row in range(len(table)):
        for c_item in cols:
            print(table[t_row][c_item].ljust(col_widths[c_item]+1), end='')
        print('')
else:
    if not args.pem and not args.text:
        print('There are no certificates for the following service(s):', ",".join(services))
