#!/bin/bash
#
# Copyright (c) 2018 The GmSSL Project.  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 acknowledgment:
#    "This product includes software developed by the GmSSL Project.
#    (http://gmssl.org/)"
#
# 4. The name "GmSSL Project" must not be used to endorse or promote
#    products derived from this software without prior written
#    permission. For written permission, please contact
#    guanzhi1980@gmail.com.
#
# 5. Products derived from this software may not be called "GmSSL"
#    nor may "GmSSL" appear in their names without prior written
#    permission of the GmSSL Project.
#
# 6. Redistributions of any form whatsoever must retain the following
#    acknowledgment:
#    "This product includes software developed by the GmSSL Project
#    (http://gmssl.org/)"
#
# THIS SOFTWARE IS PROVIDED BY THE GmSSL PROJECT ``AS IS'' AND ANY
# EXPRESSED 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 GmSSL PROJECT OR
# ITS 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.


curve=sm2p256v1
md=sm3

function set_std {
	curve=secp256k1
	md=sha256
	ca_engine=""
	user_engine=""
}

# CA ajustable parameters
ca_dn_prefix="/C=CN/ST=BJ/L=BJ/O=PKU"
ca_rootcert_days=3650
ca_usercert_days=365
#ca_engine="skf"
#ca_engine="sdf"
#user_engine="skf"
#user_engine="sdf"
user_dn_prefix="$ca_dn_prefix/OU=Sign"
user_dn_enc_prefix="$ca_dn_prefix/OU=Enc"

# The following values should not be changed.
ca_dn="$ca_dn_prefix/OU=CA/CN=PKUCA"
ca_dir=".ca"
ca_rootcert_file="$ca_dir/cacert.pem"
ca_private_dir="$ca_dir/private"
ca_rootcertkey_file="$ca_private_dir/cakey.pem"
ca_rand_file="$ca_private_dir/.rand"
ca_serial_file="$ca_dir/serial"
ca_csr_dir="$ca_dir/csr"
ca_signed_csr_dir="$ca_csr_dir/signed"
ca_rejected_csr_dir="$ca_csr_dir/rejected"
ca_cert_dir="$ca_dir/certs"
ca_newcert_dir="$ca_dir/newcerts"
ca_index_file="$ca_dir/index.txt"
ca_crl_dir="$ca_dir/crl"
ca_crl_file="$ca_crl_dir/crl.pem"
ca_crl_number_file="$ca_dir/crlnumber"
ca_crl_days=60
user_key_dir="$ca_dir/keys"

function setup {
	if [[ -n $1 ]]; then
		ca_dn=$1
	fi

	rm -fr $ca_dir
	mkdir $ca_dir
	mkdir $ca_private_dir
	mkdir $ca_crl_dir
	mkdir $ca_csr_dir
	mkdir $ca_signed_csr_dir
	mkdir $ca_rejected_csr_dir
	mkdir $ca_cert_dir
	mkdir $ca_newcert_dir
	mkdir $user_key_dir
	touch $ca_index_file
	touch $ca_rand_file
	echo 01 > $ca_serial_file
	echo 01 > $ca_crl_number_file

	case $ca_engine in
		"")
		gmssl ecparam -genkey -name $curve -noout -out $ca_rootcertkey_file
		key="-key $ca_rootcertkey_file"
		;;

		-skf)
		key="-engine skf -keyform engine -key sm2.sign"
		;;

		-sdf)
		# admin has to use sdf management tools to manually init device
		key="-engine sdf -keyform engine -key ecc_1.sign"
		;;

		*)
		echo "usage: gmca -setup <DN>"
		exit
		;;
	esac

	gmssl req -new -x509 -subj=$ca_dn -days $ca_rootcert_days -$md $key -out $ca_rootcert_file
	gmssl x509 -text -noout -in $ca_rootcert_file
}

function showstatus {
	echo "Valid certificates                  : "
	echo "Valid Distinct Subjects             : "
	echo "In Validity but Revoked Certificaes : "
	echo "Certificates near Expiration        : "
	echo "Certificates recently expired       : "
	echo "Overall Certificate Count	          : "
	echo "Overall Distinct Subjects           : "
	echo "Overall Revoked Certificates	  : "
	echo "Expired Certificates	          : "
}

function showcacert {
	gmssl x509 -text -noout -in $ca_rootcert_file
}

function getcacert {
	if [[ -n "$1" ]]; then
		out="-out $1"
	fi
	gmssl x509 -in $ca_rootcert_file $out
}

#FIXME: if common_name already exist in csr_dir or in certs_dir?
function gencsr {
	local common_name=$1
	if [[ -z $common_name ]]; then
		echo "No Common Name given"
		exit
	fi
	local subject="$user_dn_prefix/CN=$common_name"
	local csrfile="$ca_csr_dir/$common_name.csr"

	case $user_engine in
		"")
		local keyfile="$user_key_dir/$common_name.key"
		gmssl ecparam -genkey -name $curve -out "$keyfile"
		local key="-key $keyfile"
		;;

		skf)
		;;

		sdf)
		;;

		*)
		;;
	esac

	gmssl req -new -$md -subj="$subject" $key -out "$csrfile"
	gmssl req -text -in "$csrfile"
}

function listcsrs {
	for csrfile in $ca_csr_dir/*.csr; do
		if [[ -f $csrfile ]]; then
			basename $csrfile .csr
		fi
	done
}

function showcsr {
	if [[ -z "$1" ]]; then
		echo "usage: gmca -showcsr <common-name>"
		exit
	fi
	local common_name=$1
	local csrfile="$ca_csr_dir/$common_name.csr"
	#FIXME: check if $csrfile exist
	gmssl req -text -noout -in "$csrfile"
}

function getcsr {
	if [[ -z "$1" ]]; then
		echo "usage: gmca -getcsr <common-name>"
		exit
	fi
	local common_name=$1
	local csrfile="$ca_csr_dir/$common_name.csr"
	#FIXME: check if $csrfile exist
	gmssl req -in "$csrfile"
}

function signcsr {
	if [[ -z "$1" ]]; then
		echo "usage: gmca -signcsr <common-name>"
		exit
	fi
	common_name=$1
	csrfile="$ca_csr_dir/$common_name.csr"
	#FIXME: check if $csrfile exist
	#FIXME: use -subj to change csrname
	subject="$user_dn_prefix/CN=$common_name"
	gmssl ca -config ./signcsr.cnf -batch -subj=$subject -md $md -days $ca_usercert_days -outdir $ca_cert_dir -infiles "$csrfile"
}

function signenccsr {
	common_name=$1
	csrfile="$ca_csr_dir/$common_name.csr"
	subject="$user_dn_enc_prefix/CN=$common_name"
	gmssl ca -config ./signenccsr.cnf -batch -subj=$subject -md $md -days 365 -outdir $ca_cert_dir -infiles "$csrfile"
}

function gencert {
	common_name=$1
	gencsr $common_name
	signcsr $common_name
}

#FIXME: how to handle double certs?
function genenccert {
	common_name=$1
	gencsr $common_name
	signenccsr $common_name
}

function rejectcsr {
	#FIXME: check argument exist
	common_name=$1
	csrfile="$ca_csr_dir/$common_name.csr"
	#FIXME: check if $csrfile exist
	mv $csrfile $ca_rejected_csr_dir/
}

function listcerts {
	# the ca txt_db is a tab seperated text file
	# 1. Certificate validity, "V" for valid, "R" for revoked or E for expired
	# 2. Expiration datetime,
	# 3. Revokation datetime, will be null if not revoked
	# 4. Serial number
	# 5. File name of the certificate, always be "unknown"
	# 6. Certificate subject name, in oneline format
	awk -F'\t' '{print $2,$4,$6}' $ca_index_file
}

function listcertsbyname {
	#FIXME: check argument exist
	awk -F'\t' '{print $2,$4,$6}' $ca_index_file | grep $1
}

function getcertbyserial {
	#FIXME: check argument exist
	local serial=$1
	local certfile=$ca_cert_dir/$serial.pem
	gmssl x509 -in $certfile
}

function getcertbyname {
	local common_name=$1
	#FIXME: need better method than just grep
	serial=`awk -F'\t' '{print $2,$4,$6}' $ca_index_file | grep $common_name | awk '{print $2}'`
	getcertbyserial $serial
}

function listrevokereasons {
	echo " unspecified"
	echo " keyCompromise"
	echo " CACompromise"
	echo " affiliationChanged"
	echo " superseded"
	echo " cessationOfOperation"
	echo " certificateHold"
	echo " removeFromCRL (should not be used)"
}

function _revokecertfile {
	certfile=$1
	if [[ -z "$certfile" ]]; then
		echo "Usage: $0 <certfile>"
		echo ""
		exit -1
	fi
	time=`date +"%Y%m%d%H%M%S"`Z
	#reason=unspecified
	reason="keyCompromise -crl_compromise $time"
	#reason="CACompromise -crl_CA_compromise $time"
	#reason=affiliationChanged
	#reason=superseded,
	#reason=cessationOfOperation
	#reason=certificateHold
	#reason=removeFromCRL.
	#gmssl ca -config ./ca.cnf -valid $certfile
	gmssl ca -config ./ca.cnf -revoke $certfile -crl_reason $reason
	#gmssl ca -config ./ca.cnf -valid $certfile
}

function revokecertbyname {
	common_name=$1
	serial=`awk -F'\t' '{print $2,$4,$6}' $ca_index_file | grep -E "CN=$common_name$" | awk '{print $2}'`
	_revokecertfile "$ca_cert_dir/$serial.pem"
}

function revokecertbyserial {
	serial=$1
	_revokecertfile "$ca_cert_dir/$serial.pem"
}

function gencrl {
	time=`date +"%Y%m%d%H%M%S"`
	crlfile="$ca_crl_dir/$time.crl"
	gmssl ca -config ./ca.cnf -gencrl -md $md -crldays $ca_crl_days -out $crlfile
	cp $crlfile $ca_crl_file
	gmssl crl -text -noout -in $ca_crl_file -CAfile $ca_rootcert_file
}

function showcrl {
	# when use `-CAfile` option, verification result will be printed
	gmssl crl -text -noout -in $ca_crl_file #-CAfile $ca_rootcert_file
}

function getcrl {
	gmssl crl -in $ca_crl_file #-CAfile $ca_rootcert_file
}

function backup {
	time=`date +"%Y%m%d%H%M%S"`
	tar cvzf ca-$time.tar.gz $ca_dir
}

POSITIONAL=()
while [[ $# -gt 0 ]]
do
opt="$1"


#FIXME: show help info, get more options
case $opt in
	-verbose)
		verbose=yes
		shift
		shift
		;;
	-setup)
		dn="$2"
		setup $dn
		shift
		shift
		;;
	-showcacert)
		showcacert
		shift
		;;
	-getcacert)
		file="$2"
		getcacert $file
		shift
		shift
		;;
	-showstatus)
		showstatus
		shift
		;;
	-gencsr)
		common_name="$2"
		gencsr "$common_name"
		shift
		shift
		;;
	-listcsrs)
		listcsrs
		shift
		;;
	-showcsr)
		common_name="$2"
		showcsr "$common_name"
		shift
		shift
		;;
	-getcsr)
		common_name="$2"
		getcsr "$common_name"
		shift
		shift
		;;
	-signcsr)
		common_name="$2"
		signcsr "$common_name"
		shift
		shift
		;;
	-signenccsr)
		common_name="$2"
		signenccsr "$common_name"
		shift
		shift
		;;
	-rejectcsr)
		common_name="$2"
		rejectcsr "$common_name"
		shift
		shift
		;;
	-gencert)
		common_name="$2"
		gencert $common_name
		shift
		shift
		;;
	-genenccert)
		common_name="$2"
		genenccert $common_name
		shift
		shift
		;;
	-listcerts)
		listcerts
		shift
		;;
	-listcertsbyname)
		name="$2"
		listcertsbyname $name
		shift
		shift
		;;
	-getcertbyserial)
		serial="$2"
		getcertbyserial $serial
		shift
		shift
		;;
	-getcertbyname)
		name="$2"
		getcertbyname $name
		shift
		shift
		;;
	-showcert)
		cert="$2"
		showcert "$cert"
		shift
		shift
		;;
	-revokereasons)
		revokereasons
		shift
		;;
	-revokecertbyname)
		name="$2"
		revokecertbyname "$name"
		shift
		shift
		;;
	-revokecert)
		certfile="$2"
		revokebycert "$certfile"
		shift
		shift
		;;
	-revokecertbyserial)
		serial="$2"
		revokecertbyserial $serial
		shift
		shift
		;;
	-gencrl)
		gencrl
		shift
		;;
	-crl)
		getcrl
		shift
		;;
	-showcrl)
		showcrl
		shift
		;;
	-getcrl)
		getcrl
		shift
		;;
	-backup)
		backup
		shift
		;;
	*)
		echo "usage: ...."
		POSITIONAL+=("$1")
		shift
		;;
esac
done
set -- "${POSITIONAL[@]}"

if [[ -n $1 ]]; then
	echo "Last line of file specified as non-opt/last argument:"
fi
