#!/bin/bash set -e export KEYSER_VERSION="0.0.8" export KEYSER_LAYOUT_VERION="1" help(){ echo """ Usage keyser Available Commands cacert Create a certificate authority. cacert_list List the certificate authorities registered in the vault repository. cacert_view Print a certificate authority. cert Generate a certificate. cert_check Check a hostname certificate against the certificate authority. cert_check_from_file Check a certificate against the certificate authority and its key. cert_export Export a certificate and its private key into a target directory. cert_list List the certificates registered in the vault repository. cert_view Print a certificate. csr_create Create a certificate signing request. csr_sign Sign a CSR given its fqdn name. csr_sign_from_file Sign a CSR given its path. csr_view Print a CSR. init Initialize a new vault repository. help Print the Keyser help. version Print the Keyser version. Environment variables KEYSER_VAULT_DIR Keys directory storage location. KEYSER_GPG_MODE GPG encryption mode, only \"symmetric\" is supported. KEYSER_GPG_PASSPHRASE Passphrase used for GPG encryption or leave empty for no encryption. Example to view a certificate keyser cacert domain.com keyser cert test.domain.com keyser cert_view test.domain.com Example with intermediate certificate keyser cacert domain-1.com keyser cert domain-2.com domain-1.com keyser cert domain-3.com domain-2.com keyser cert_check_from_file test.domain.com ./vault/com/domain-3/cert.pem ./vault/com/domain-3/ca.crt Example with symetric encryption export KEYSER_GPG_PASSPHRASE=secret keyser cacert domain-1.com """ } help_init(){ echo """ Description Initialize a new vault repository. Usage keyser init Options The command has no option. A new directory is created at the vault location defined by the \`KEYSER_VAULT_DIR\` environmental variable. GPG encryption is automatically enabled with if the \`KEYSER_GPG_PASSPHRASE\` environmental variable is set. """ } init(){ local OPTIND help while getopts ":h" option; do case $option in h) help=1;; \?) echo "$OPTARG : option invalide" return 1;; esac done # shift $((OPTIND-1)) if [[ -n "$help" ]]; then help_init; return 0; fi KEYSER_VAULT_DIR="${KEYSER_VAULT_DIR:-./vault}" # KEYSER_GPG_MODE="${KEYSER_GPG_MODE}" # KEYSER_GPG_PASSPHRASE="${KEYSER_GPG_PASSPHRASE}" # Validation if [[ -n "$KEYSER_GPG_MODE" ]] && [[ $KEYSER_GPG_MODE != "symmetric" ]]; then >&2 echo 'Invalid GPG mode, use "symmetric" or leave it empty to disable encryption.' help return 1 fi # Storage preparation if [[ ! -d "$KEYSER_VAULT_DIR" ]]; then echo "Vault created: $KEYSER_VAULT_DIR" mkdir -m 700 -p "$KEYSER_VAULT_DIR" fi if [[ -n "$KEYSER_GPG_PASSPHRASE" ]] && [[ ! -f "$KEYSER_VAULT_DIR/.gitignore" ]]; then echo '*.pem' > "$KEYSER_VAULT_DIR/.gitignore" echo 'Git .ignore file created:' "$KEYSER_VAULT_DIR/.gitignore" fi # Vault metadata (introduced in version 0.0.8) if [[ ! -f "$KEYSER_VAULT_DIR/METADATA" ]]; then cat < "$KEYSER_VAULT_DIR/METADATA" VERSION=$KEYSER_VERSION LAYOUT_VERSION=1 META fi while IFS='=' read -r var value; do printf -v "METADATA_$var" %s "$value" done < "$KEYSER_VAULT_DIR/METADATA" } help_cacert(){ echo """ Description Create a certificate authority. Usage cacert -acdefhlo Options -a Comma-separated list of SAN's IP adresses. -c Country of the issuer. -d Comma-separated list of SAN's DNS domains. -e Email of the issuer. -f Overwrite existing certificate files if present. -h Print the command help. -l Location of the issuer. -o Organization of the issuer. fqdn FQDN of the registered certificate. Example keyser cacert \ -c FR \ -d local,localhost -e no-reply@adaltas.com \ -i 127.0.0.1 -l Paris \ -o Adaltas \ adaltas.com """ } cacert(){ utils_vault_created # Parse flags local OPTIND force help local country email location organization local address dns while getopts ":a:c:d:e:fhl:o:" option; do case $option in a) address="$OPTARG";; c) country="$OPTARG";; d) dns="$OPTARG";; e) email="$OPTARG";; f) force=1;; h) help=1;; l) location="$OPTARG";; o) organization="$OPTARG";; \?) echo "Invalid option -$OPTARG" return 1;; :) echo "Option -$OPTARG requires an argument." >&2 return 1;; esac done shift $((OPTIND-1)) if [[ -n "$help" ]]; then help_cacert; return 0; fi local fqdn=$1 # Validation if [[ -z "$country" ]]; then echo 'Country is missing from arguments.'; help_cacert; return 1; fi if [[ -z "$email" ]]; then echo 'Email is missing from arguments.'; help_cacert; return 1; fi if [[ -z "$location" ]]; then echo 'Location is missing from arguments.'; help_cacert; return 1; fi if [[ -z "$organization" ]]; then echo 'Organization is missing from arguments.'; help_cacert; return 1; fi if [[ -z "$fqdn" ]]; then echo 'FQDN is missing from arguments.'; help_cacert; return 1; fi local -r fqdn_dir="$KEYSER_VAULT_DIR/$(utils_reverse "$fqdn")" local key_file="$fqdn_dir/key.pem" if [[ -n "$KEYSER_GPG_PASSPHRASE" ]]; then key_file+='.gpg' fi if [[ -d "$fqdn_dir" ]]; then if [[ -n "$force" ]]; then rm -r "$fqdn_dir" else echo 'CA certificate files already exists.'; help_cacert; return 1; fi fi # Prepare configuration template mkdir -m 700 "$fqdn_dir" # cp -rp $pwd/lib/ca.cnf $fqdn_dir/ca.cnf utils_write_ca_cnf "$fqdn_dir/ca.cnf" # RSA Private key (create "ca.key.pem") openssl genrsa -out "$fqdn_dir/key.pem" 2048 # Self-signed (with the key previously generated) root CA certificate (create "ca.crt.pem") # man: The req command primarily creates and processes certificate requests in PKCS#10 format. local -r san=$(utils_opt_san "$dns" "$address") openssl req -x509 -new -sha256 \ -config "$fqdn_dir/ca.cnf" \ -subj "/C=$country/O=$organization/L=$location/CN=$fqdn/emailAddress=$email" \ ${san:+-addext "$san"} \ -addext "nsComment= OpenSSL Generated Certificate by Keyser" \ -key "$fqdn_dir/key.pem" -days 7300 \ -out "$fqdn_dir/cert.pem" echo "Certificate authority created: $fqdn_dir/cert.pem" if [[ -z "$KEYSER_GPG_PASSPHRASE" ]]; then echo "Certificate key created: $fqdn_dir/key.pem" else utils_encrypt "$fqdn_dir/key.pem" "$fqdn_dir/key.pem.gpg" > /dev/null rm "$fqdn_dir/key.pem" echo "Certificate key created: $fqdn_dir/key.pem.gpg" fi } help_cacert_list(){ echo """ Description List the certificate authorities registered in the vault repository. Usage keyser cacert_list """ } cacert_list(){ utils_vault_created # Parse flags local OPTIND help while getopts ":h" option; do case $option in h) help=1;; \?) echo "Invalid option -$OPTARG" return 1;; :) echo "Option -$OPTARG requires an argument." >&2 return 1;; esac done shift $((OPTIND-1)) if [[ -n "$help" ]]; then help_cacert_list; return 0; fi # Filter all cacerts based an the CA configuration file # Using cat to always get valid exit code, even when no file is found files=$(find "$KEYSER_VAULT_DIR"/*/ca.cnf 2>/dev/null | cat) if [[ ${#files} == 0 ]]; then echo 'There are no registered certificate autority in this vault.' else for fqdn in $files; do fqdn=$(dirname "$fqdn") fqdn=$(basename "$fqdn") fqdn=$(utils_reverse "$fqdn") echo "$fqdn" done fi } help_cacert_view(){ echo """ Description Print a certificate authority. Usage keyser cacert_view -hst Options -h Print the command help. -s Print the subject information only. -t Print text and fingerprint information. fqdn FQDN of the registered certificate. The command is an alias of \"cert_view\". """ } cacert_view(){ utils_vault_created # Parse flags local OPTIND help local subject text while getopts ":hst" option; do case $option in h) help=1;; s) subject=1;; t) text=1;; \?) echo "Invalid option -$OPTARG" return 1;; :) echo "Option -$OPTARG requires an argument." >&2 return 1;; esac done if [[ -n "$help" ]]; then help_cacert_view; return 0; fi cert_view "$@" } help_cert(){ echo """ Description Generate a certificate. Usage keyser cert -acdeihlo [] Options -a Comma-separated list of SAN's IP adresses. -c Country of the issuer. -d Comma-separated list of SAN's DNS domains. -e Email of the issuer. -i Create a CSR for an intermediate certificate. -h Print the command help. -l Location of the issuer. -o Organization of the issuer. fqdn FQDN of the registered certificate. ca_fqdn Parent FQDN used to sign the CSR, optional. Generate the certificate and private key for a given hostname. The command combines the csr_create and csr_sign commands for conveniency. The parameter is optional. If not defined, it is currently derived from the sub domain. If not present in the vault repository, the command exit on error. Use the \`cacert\` command to generate a self-signed certificate. The subject information (country, email, location and organization) default to the CA certificate. """ } cert(){ utils_vault_created # Parse flags local OPTIND intermediate help local country email location organization while getopts ":a:c:d:e:ihl:o:" option; do case $option in a) address="$OPTARG";; c) country="$OPTARG";; d) dns="$OPTARG";; e) email="$OPTARG";; i) intermediate='-i';; h) help=1;; l) location="$OPTARG";; o) organization="$OPTARG";; \?) echo "Invalid option -$OPTARG" return 1;; :) echo "Option -$OPTARG requires an argument." >&2 return 1;; esac done shift $((OPTIND-1)) if [[ -n "$help" ]]; then help_cert; return 0; fi local fqdn=$1 local ca_fqdn=$2 if [[ -z "$ca_fqdn" ]]; then ca_fqdn=${fqdn#*.} fi # Validation if [[ -z "$fqdn" ]]; then echo 'FQDN is missing from arguments.'; help_cert; return 1; fi # Argument discovery local -r ca_fqdn_dir=$KEYSER_VAULT_DIR/$(utils_reverse "$ca_fqdn") if [[ ! -d "$ca_fqdn_dir" ]]; then echo "Parent FQDN is not registered in the vault repository, use \`cacert\` to generate a self-signed certiciate." exit 1 fi # Extract default CA certificate information local -r subject=$(openssl x509 -noout -subject -nameopt sep_multiline -in "$ca_fqdn_dir"/cert.pem); [[ -z "$country" ]] && country=$(echo "$subject" | grep C= | cut -d'=' -f2); [[ -z "$email" ]] && email=$(echo "$subject" | grep emailAddress= | cut -d'=' -f2); [[ -z "$location" ]] && location=$(echo "$subject" | grep L= | cut -d'=' -f2); [[ -z "$organization" ]] && organization=$(echo "$subject" | grep O= | cut -d'=' -f2); # Certificate generation csr_create \ ${address:+"-a $address"} ${dns:+"-d $dns"} \ -c "$country" -e "$email" -l "$location" -o "$organization" \ "$fqdn" csr_sign \ ${address:+"-a $address"} ${dns:+"-d $dns"} \ $intermediate \ "$fqdn" "${@:2}" } help_cert_check(){ echo """ Description Check a hostname certificate against the certificate authority. Usage keyser cert_check Options -h Print the command help. fqdn FQDN of the registered certificate. Notes Internally, the command localize the certificate inside its store. Then, it call the cert_check_from_file command. """ } cert_check(){ utils_vault_created # Parse flags local OPTIND help while getopts ":h" option; do case $option in h) help=1;; \?) echo "Invalid option -$OPTARG" return 1;; :) echo "Option -$OPTARG requires an argument." >&2 return 1;; esac done shift $((OPTIND-1)) if [[ -n "$help" ]]; then help_cert_check; return 0; fi local fqdn=$1 # Validation if [[ -z "$fqdn" ]]; then echo 'FQDN is missing from arguments.'; help_cert_check; return 1; fi local -r fqdn_dir=$KEYSER_VAULT_DIR/$(utils_reverse "$fqdn") local cert_file="$fqdn_dir"/cert.pem local cacert_file="$fqdn_dir"/ca.crt local key_file="$fqdn_dir"/key.pem if [[ -n "$KEYSER_GPG_PASSPHRASE" ]]; then utils_decrypt "$fqdn_dir"/key.pem.gpg "$key_file" > /dev/null fi cert_check_from_file -a "$cacert_file" -k "$key_file" "$cert_file" cert_check_from_file_code=$? if [[ -n "$KEYSER_GPG_PASSPHRASE" ]]; then rm "$key_file" fi return "$cert_check_from_file_code" } help_cert_check_from_file(){ echo """ Description Check a certificate against the certificate authority and its key. Usage keyser cert_check_from_file [-akh] Options -a Path to the certificate authority. -k Path to the key file. -h Print the command help. cert_file Path to the certificate. Note Internally, certificate authority validation uses \`openssl verify\`. To validate the certificate with its key, we extract and compare the modulus values. """ } cert_check_from_file(){ utils_vault_created # Parse flags local OPTIND help while getopts ":a:k:h" option; do case $option in a) cacert_file="$OPTARG";; k) key_file="$OPTARG";; h) help=1;; \?) echo "Invalid option -$OPTARG" return 1;; :) echo "Option -$OPTARG requires an argument." >&2 return 1;; esac done shift $((OPTIND-1)) if [[ -n "$help" ]]; then help_cert_check_from_file; return 0; fi local cert_file=$1 echo "-- key_file $key_file" echo "-- cert_file $cert_file" # Validation if [[ -z "$cert_file" ]]; then echo 'Certificate file path is missing from arguments.'; help_cert_check_from_file; return 1; fi if [[ ! -f "$cert_file" ]]; then echo "Certificate file does not exist: \"$cert_file\"."; return 1; fi if [[ -z "$cacert_file" ]]; then # Discover the certificate in the local vault # FQDN extraction local -r fqdn=$(openssl x509 -noout -subject -nameopt sep_multiline -in "$cert_file" | grep CN= | cut -d'=' -f2); local ca_fqdn=${fqdn#*.} local -r ca_fqdn_dir="$KEYSER_VAULT_DIR"/$(utils_reverse "$ca_fqdn") cacert_file="$ca_fqdn_dir"/cert.pem fi openssl verify \ -CAfile "$cacert_file" \ "$cert_file" \ >/dev/null \ 2>&1 local verifyCode=$? if [[ $verifyCode != 0 ]]; then echo "Verification failed: \`openssl verify\` exit code is $verifyCode." return 1 fi if [[ -n "$key_file" ]]; then utils_openssl_modulus "$key_file" "$cert_file" if [[ $? != 0 ]]; then echo "Verification failed: modulus signature don't match." return 1 fi fi echo 'Certificate is valid.' } help_cert_export(){ echo """ Description Export a certificate and its private key into a target directory. Usage keyser cert_export -cfh Options -c Create the directory and its itermediate directories. -f Overwrite certificate and key files if present. -h Print the command help. fqdn FQDN of the registered certificate. target Target directory. The certificate file is an SSL certificate chain. It contains the list of certificates starting with the FQDN certificate and ending with the root certificate. Two files are created. The key is named .key.pem. The certificate chain is named .cert.pem. Unless \"-f\" is provided, the certificate and key files must not exist. """ } cert_export(){ utils_vault_created # Parse flags local OPTIND help local create force while getopts ":cfh" option; do case $option in c) create=1;; f) force=1;; h) help=1;; \?) echo "Invalid option -$OPTARG" return 1;; :) echo "Option -$OPTARG requires an argument." >&2 return 1;; esac done shift $((OPTIND-1)) if [[ -n "$help" ]]; then help_cert_export; return 0; fi local fqdn=$1 local target=$2 local -r target_key_file="$target/$(utils_reverse "$fqdn").key.pem" local -r target_cert_file="$target/$(utils_reverse "$fqdn").cert.pem" # Validation if [[ -z "$fqdn" ]]; then echo 'FQDN is missing from arguments.'; help_cert_export; return 1; fi if [[ -n "$create" ]]; then mkdir -p "$target" fi if [[ ! -d "$target" ]]; then echo 'Target is not a directory.'; help_cert_export; return 1; fi if [[ -z "$force" && -e "$target_key_file" ]]; then if [[ -f "$target_key_file" ]]; then echo 'Target key file already exists.'; help_cert_export; return 1; else echo 'Target key is not a file.'; help_cert_export; return 1; fi fi if [[ -z "$force" && -e "$target_cert_file" ]]; then if [[ -z "$force" && ! -f "$target_cert_file" ]]; then echo 'Target certificate is not a file.'; help_cert_export; return 1; else echo 'Target certificate file already exists.'; help_cert_export; return 1; fi fi # Work local -r fqdn_dir="$KEYSER_VAULT_DIR/$(utils_reverse "$fqdn")" local key_file="$fqdn_dir"/key.pem local key_file_gpg="$fqdn_dir"/key.pem.gpg local cert_file="$fqdn_dir"/cert.pem local cacert_file="$fqdn_dir"/ca.crt local -r key_managed=$([[ ( -n "$KEYSER_GPG_PASSPHRASE" && -f "$key_file_gpg" ) || -f "$key_file" ]] && echo 1) if [[ -n "$KEYSER_GPG_PASSPHRASE" && -n $key_managed ]]; then utils_decrypt "$fqdn_dir"/key.pem.gpg "$key_file" > /dev/null fi # Convert x509 certs to PKCS12 # openssl pkcs12 -export -out testsign.p12 -inkey testsign.key -in testsign.crt # openssl pkcs12 -export -out rootCA.p12 -inkey rootCA.key -in rootCA.crt # File export [[ -n $key_managed ]] && cp -rp "$key_file" "$target_key_file" cp -rp "$cert_file" "$target_cert_file" cat "$cacert_file" >> "$target_cert_file" # Cleanup [[ -n "$KEYSER_GPG_PASSPHRASE" && -n $key_managed ]] && rm "$fqdn_dir"/key.pem # Print information [[ -n $key_managed ]] && echo '-- key file:' "$key_file" echo '-- cert file:' "$cert_file" } help_cert_list(){ echo """ Description List the certificates registered in the vault repository. Usage keyser cert_list """ } cert_list(){ utils_vault_created # Parse flags local OPTIND help while getopts ":h" option; do case $option in h) help=1;; \?) echo "Invalid option -$OPTARG" return 1;; :) echo "Option -$OPTARG requires an argument." >&2 return 1;; esac done shift $((OPTIND-1)) if [[ -n "$help" ]]; then help_cert_list; return 0; fi # Filter all cacerts based an the CA configuration file # Using cat to always get valid exit code, even when no file is found files=$(find "$KEYSER_VAULT_DIR"/*/ca.crt 2>/dev/null | cat) if [[ ${#files} == 0 ]]; then echo 'There are no registered certificate in this vault.' else for fqdn in $files; do fqdn=$(dirname "$fqdn") fqdn=$(basename "$fqdn") fqdn=$(utils_reverse "$fqdn") echo "$fqdn" done fi } help_cert_view(){ echo """ Description Print a certificate. Usage keyser cert_view -hst Options -h Print the command help. -s Print the subject information only. -t Print text and fingerprint information. fqdn FQDN of the registered certificate. Print the detailed information of a Certificate Signing Request (CSR) file. """ } cert_view(){ utils_vault_created # Parse flags local OPTIND help local subject text while getopts ":hst" option; do case $option in h) help=1;; s) subject=1;; t) text=1;; \?) echo "Invalid option -$OPTARG" return 1;; :) echo "Option -$OPTARG requires an argument." >&2 return 1;; esac done shift $((OPTIND-1)) if [[ -n "$help" ]]; then help_cert_view; return 0; fi local fqdn=$1 # Validation if [[ -z "$fqdn" ]]; then echo 'FQDN is missing from arguments.'; help_cert_view; return 1; fi local -r fqdn_dir=$KEYSER_VAULT_DIR/$(utils_reverse "$fqdn") if [[ ! -f "$fqdn_dir/cert.pem" ]]; then echo "Certificate file for domain \"$fqdn\" does not exist."; help_cert_view; return 1; fi # Obtain certificate information if [[ -n $subject ]]; then openssl x509 -noout -subject \ -in "$fqdn_dir/cert.pem" elif [[ -n $text ]]; then openssl x509 -noout -text -fingerprint \ -in "$fqdn_dir/cert.pem" else openssl x509 \ -in "$fqdn_dir/cert.pem" fi } help_csr_create(){ echo """ Description Create a certificate signing request. Usage keyser csr_create -acdehlo Options -a Comma-separated list of SAN's IP adresses. -c Country of the issuer. -d Comma-separated list of SAN's DNS domains. -e Email of the issuer. -h Print the command help. -l Location of the issuer. -o Organization of the issuer. fqdn FQDN of the generated CSR, used for the subject CN. Generate a key and its certificate signing request. """ } csr_create(){ utils_vault_created # Parse flags local OPTIND help local country email location organization local address dns while getopts ":a:c:d:e:hl:o:" option; do case $option in a) address="$OPTARG";; c) country="$OPTARG";; d) dns="$OPTARG";; e) email="$OPTARG";; h) help=1;; l) location="$OPTARG";; o) organization="$OPTARG";; \?) echo "Invalid option -$OPTARG" return 1;; :) echo "Option -$OPTARG requires an argument." >&2 return 1;; esac done shift $((OPTIND-1)) if [[ -n "$help" ]]; then help_csr_create; return 0; fi local fqdn=$1 # Validation if [[ -z "$country" ]]; then echo 'Country is missing from arguments.'; help_csr_create; return 1; fi if [[ -z "$email" ]]; then echo 'Email is missing from arguments.'; help_csr_create; return 1; fi if [[ -z "$location" ]]; then echo 'Location is missing from arguments.'; help_csr_create; return 1; fi if [[ -z "$organization" ]]; then echo 'Organization is missing from arguments.'; help_csr_create; return 1; fi if [[ -z "$fqdn" ]]; then echo 'FQDN is missing from arguments.'; help_csr_create; return 1; fi local -r fqdn_dir="$KEYSER_VAULT_DIR/$(utils_reverse "$fqdn")" # to view the CSR: `openssl req -in toto.cert.csr -noout -text` # Sign the CSR (create "hadoop.cert.pem") if [ -d "$fqdn_dir" ]; then echo 'FQDN repository already exists.'; help_csr_create; return 1; fi mkdir -m 700 -p "$fqdn_dir" local -r san=$(utils_opt_san "$dns" "$address") openssl req -newkey rsa:2048 -sha256 -nodes \ -out "$fqdn_dir/cert.csr" \ -keyout "$fqdn_dir/key.pem" \ -subj "/C=$country/O=$organization/L=$location/CN=${fqdn}/emailAddress=$email" \ ${san:+-addext "$san"} \ 2>/dev/null [[ $? != 0 ]] && return 1 if [[ -z "$KEYSER_GPG_PASSPHRASE" ]]; then echo "Key created in: $fqdn_dir/key.pem" else utils_encrypt "$fqdn_dir/key.pem" "$fqdn_dir/key.pem.gpg" > /dev/null rm "$fqdn_dir/key.pem" echo "Key created in: $fqdn_dir/key.pem.gpg" fi # CSR can be verify with the command # openssl req -text -noout -verify -in exemple.csr echo "CSR created in: $fqdn_dir/cert.csr" } help_csr_sign(){ echo """ Description Sign a CSR given its fqdn name. Usage keyser csr_sign -adih [] Options -a Comma-separated list of SAN's IP adresses. -d Comma-separated list of SAN's DNS domains. -i Create a CSR for an intermediate certificate. -h Print the command help. fqdn FQDN of the registered certificate. ca_fqdn FQDN of the certificate authority used to sign, optional. Generate a certificate for a managed FQDN with an existing CSR. Internally, it uses the \`csr_sign_from_file\` command. The CSR is delete upon completion. """ } csr_sign(){ utils_vault_created # Parse flags local OPTIND intermediate help while getopts ":a:d:ih" option; do case $option in a) address="$OPTARG";; d) dns="$OPTARG";; i) intermediate='-i';; h) help=1;; \?) echo "Invalid option -$OPTARG" return 1;; :) echo "Option -$OPTARG requires an argument." >&2 return 1;; esac done shift $((OPTIND-1)) if [[ -n "$help" ]]; then help_csr_sign; return 0; fi local fqdn=$1 # Validation if [[ -z "$fqdn" ]]; then echo 'FQDN is missing from arguments.'; help_csr_sign; return 1; fi local -r fqdn_dir="$KEYSER_VAULT_DIR/$(utils_reverse "$fqdn")" local csr_file="$fqdn_dir/cert.csr" if [[ ! -f $csr_file ]]; then echo "CSR file does not exist: \"$csr_file\"."; help_csr_sign; return 1; fi # Sign certificate csr_sign_from_file \ ${address:+"-a $address"} ${dns:+"-d $dns"} \ $intermediate \ "$csr_file" "$2" # Dispose CSR file rm "$csr_file" } help_csr_sign_from_file(){ echo """ Description Sign a CSR given its path. Usage keyser csr_sign_from_file -adih [] Options -a Comma-separated list of SAN's IP adresses. -d Comma-separated list of SAN's DNS domains. -i Create an intermediate certificate. -h Print the command help. csr_file Path to the CSR to sign. ca_fqdn FQDN of the certificate authority used to sign, optional. Sign the provided certificate signing request (CSR) file. The FQDN is extracted from the subject CN and is required. It must be managed by the vault. The Certificate Authority FQDN is optionnal. When not provided, it is obtain from the FQDN by removing the hostname part. """ } csr_sign_from_file(){ utils_vault_created # Parse flags local OPTIND help local address dns intermediate while getopts ":a:d:ih" option; do case $option in a) address="$OPTARG";; d) dns="$OPTARG";; i) intermediate='-i';; h) help=1;; \?) echo "Invalid option -$OPTARG" return 1;; :) echo "Option -$OPTARG requires an argument." >&2 return 1;; esac done shift $((OPTIND-1)) if [[ -n "$help" ]]; then help_csr_sign_from_file; return 0; fi local csr_file=$1 local ca_fqdn=$2 # Validation if [[ ! -f $csr_file ]]; then echo "CSR file does not exist: \"$csr_file\"."; help_csr_sign_from_file; return 1; fi # FQDN extraction and validation local -r fqdn=$(openssl req -noout -subject -nameopt sep_multiline -in "$csr_file" | grep CN= | cut -d'=' -f2); if [[ -z "$fqdn" ]]; then echo "Failed to extract the fqdn from the subject CN."; help_csr_sign_from_file; return 1; fi if [[ -z "$ca_fqdn" ]]; then ca_fqdn="${fqdn#*.}" fi local -r fqdn_dir="$KEYSER_VAULT_DIR/$(utils_reverse "$fqdn")" local -r ca_fqdn_dir="$KEYSER_VAULT_DIR/$(utils_reverse "$ca_fqdn")" if [[ ! -d $ca_fqdn_dir ]]; then echo "Missing key for domain \"${ca_fqdn}\" to sign the CSR"; help_csr_sign_from_file; return 1; fi # Creating directory when signing an external CSR mkdir -p "$fqdn_dir" # Place a copy of the CSR file if [[ "$csr_file" != "$fqdn_dir/${csr_file##*/}" ]]; then cp -rp "$csr_file" "$fqdn_dir/${csr_file##*/}" fi # Copy the certificate authority # Note, naming is inspired by ipa if [[ -f "$ca_fqdn_dir/cert.pem" ]]; then echo '' > "$fqdn_dir/ca.crt" cat "$ca_fqdn_dir/cert.pem" >> "$fqdn_dir/ca.crt" if [[ -f "$ca_fqdn_dir/ca.crt" ]]; then cat "$ca_fqdn_dir/ca.crt" >> "$fqdn_dir/ca.crt" fi fi # Sign the CSR if [[ -n "$KEYSER_GPG_PASSPHRASE" ]]; then [[ -f "$ca_fqdn_dir/key.pem" ]] && rm "$ca_fqdn_dir/key.pem" utils_decrypt "$ca_fqdn_dir/key.pem.gpg" "$ca_fqdn_dir/key.pem" > /dev/null fi utils_write_sign_cnf ${address:+"-a $address"} ${dns:+"-d $dns"} $intermediate $fqdn_dir/sign.cnf # See [Requirements](https://support.apple.com/en-us/103769) for trusted certificates in iOS 13 and macOS 10.15 # TLS server certificates must have a validity period of 825 days or fewer (as expressed in the NotBefore and NotAfter fields of the certificate). # If you want to copy the extensions from the csr to the crt, add the option `-copy_extensions=copyall` error=$( openssl x509 -req -sha256 -days 825 \ -CA "$ca_fqdn_dir/cert.pem" -CAkey "$ca_fqdn_dir/key.pem" \ -CAcreateserial -CAserial "$ca_fqdn_dir/ca.seq" \ -extfile "$fqdn_dir/sign.cnf" \ -in "$csr_file" \ -out "$fqdn_dir/cert.pem" \ 2>&1 >/dev/null ) local exit_code=$? if [[ -n "$KEYSER_GPG_PASSPHRASE" ]]; then rm "$ca_fqdn_dir/key.pem" fi if [[ $exit_code != 0 ]]; then echo "$error"; return 1; fi echo "Certificate authority in: $fqdn_dir/ca.crt" echo "Certificate created in: $fqdn_dir/cert.pem" } help_csr_view(){ echo """ Description Print a CSR. Usage keyser csr_view -hs Options -h Print the command help. -s Print the subject information only. fqdn FQDN of the registered certificate. Print the detailed information of a Certificate Signing Request (CSR) stored inside the vault. """ } csr_view(){ utils_vault_created # Parse flags local OPTIND help local subject while getopts ":hs" option; do case $option in h) help=1;; s) subject=1;; \?) echo "Invalid option -$OPTARG" return 1;; esac done shift $((OPTIND-1)) if [[ -n "$help" ]]; then help_csr_view; return 0; fi local fqdn=$1 # Validation if [[ -z "$fqdn" ]]; then echo 'FQDN is missing from arguments.'; help_csr_view; return 1; fi local -r fqdn_dir=$KEYSER_VAULT_DIR/$(utils_reverse "$fqdn") if [[ ! -f "$fqdn_dir/cert.csr" ]]; then echo "CSR file for domain \"$fqdn\" does not exist."; help_csr_view; return 1; fi # Obtain certificate information if [[ -n $subject ]]; then openssl req -noout -subject \ -in "$fqdn_dir/cert.csr" else openssl req -noout -text \ -in "$fqdn_dir/cert.csr" fi } version(){ echo "Keyser version \"${KEYSER_VERSION}\"." } utils_reverse(){ IFS=. read -ra line <<< "$1" ((x=${#line[@]}-1)) while [ "$x" -ge 0 ]; do echo -n "${line[$x]}"; [ $x != 0 ] && echo -n '.'; (( x-- )); echo -n '' done } utils_tld(){ local fqdn=$1 echo -n "${fqdn#*.}" } utils_domain(){ local fqdn=$1 echo -n "${fqdn%%.*}" } utils_encrypt(){ local source=$1 local target=$2 if [[ ! -n "$KEYSER_GPG_PASSPHRASE" ]]; then echo "$source" else if [[ ! -f "$source" ]]; then echo 'No such file to encrypt.'; exit 1; fi if [[ -f "$target" ]]; then rm "$target"; fi gpg --batch --passphrase "$KEYSER_GPG_PASSPHRASE" --output "$target" --symmetric "$source" [[ $? != 0 ]] && echo 'GPG command failed' && return 1 echo "$target" fi } utils_decrypt(){ local source=$1 local target=$2 if [[ ! -n "$KEYSER_GPG_PASSPHRASE" ]]; then echo "$source" else if [[ ! -f "$source" ]]; then echo 'No such file to decrypt.'; exit 1; fi if [[ -f "$target" ]]; then rm "$target"; fi gpg --batch --passphrase "$KEYSER_GPG_PASSPHRASE" --output "$target" --decrypt "$source" 2>/dev/null echo "$target" fi } utils_openssl_modulus(){ # Check if Private key and Certificate match # See https://support.openprovider.eu/hc/en-us/articles/360038204734-What-to-do-when-my-Private-key-and-Certificate-do-not-match local key_file=$1 local cert_file=$2 local -r keyModulus=$(openssl rsa -modulus -noout -in "$key_file") local -r certModulus=$(openssl x509 -modulus -noout -in "$cert_file") if [[ "$keyModulus" != "$certModulus" ]]; then return 1 fi } utils_opt_san(){ local dns=$1 local address=$2 local san if [[ -n "$dns" ]] || [[ -n "$address" ]]; then san='subjectAltName= ' if [[ -n "$dns" ]]; then san+="DNS: ${dns//,/, DNS: }" fi if [[ -n "$dns" ]] && [[ -n "$address" ]]; then san+=', ' fi if [[ -n "$address" ]]; then san+="IP: ${address//,/, address: }" fi fi echo "$san" } utils_vault_created(){ if [[ ! -d $KEYSER_VAULT_DIR ]]; then echo "Vault not initialized, run \`keyser init\` first." >&2 exit 1 fi } utils_write_ca_cnf(){ # On modern operating systems, RANDFILE is generally no longer necessary. # See https://www.openssl.org/docs/man1.1.1/man1/rand.html cat <<-'EOF' > "$1" HOME = . [ ca ] default_ca = CA_default # The default ca section [ CA_default ] default_days = 1000 # How long to certify for default_crl_days = 30 # How long before next CRL default_md = sha256 # Use public key default MD preserve = no # Keep passed DN ordering x509_extensions = x509_ext # The extensions to add to the cert email_in_dn = no # Don't concat the email in the DN copy_extensions = copy # Required to copy SANs from CSR to cert [ req ] default_bits = 2048 default_keyfile = cakey.pem distinguished_name = subject x509_extensions = x509_ext string_mask = utf8only [ subject ] [ x509_ext ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always, issuer basicConstraints = critical, CA:true keyUsage = keyCertSign, cRLSign EOF } help_utils_write_sign_cnf(){ echo """ Description Generate a sign configuration. Usage keyser utils_write_sign_cnf Options -a Comma-separated list of SAN's IP adresses. -d Comma-separated list of SAN's DNS domains. -i Create an intermediate certificate. -h Print the command help. Write a configuration file in the target destination used to sign a certificate request. """ } utils_write_sign_cnf(){ # Parse flags local OPTIND help local address dns intermediate while getopts ":a:d:ih" option; do case $option in a) address="$OPTARG";; d) dns="$OPTARG";; i) intermediate='TRUE';; h) help=1;; \?) echo "Invalid option -$OPTARG" return 1;; esac done shift $((OPTIND-1)) if [[ -n "$help" ]]; then help_utils_write_sign_cnf; return 0; fi local -r san=$(utils_opt_san "$dns" "$address") cat <<-EOF > "$1" subjectKeyIdentifier=hash authorityKeyIdentifier=keyid,issuer basicConstraints=critical,CA:${intermediate:=FALSE},pathlen:1 ${san} EOF } if [[ "${BASH_SOURCE[0]}" = "$0" ]]; then case "$1" in cacert) init && cacert "${@:2}" ;; cacert_list) init && cacert_list "${@:2}" ;; cacert_view) init && cacert_view "${@:2}" ;; cert) init && cert "${@:2}" ;; cert_check) init && cert_check "${@:2}" ;; cert_check_from_file) init && cert_check_from_file "${@:2}" ;; cert_export) init && cert_export "${@:2}" ;; cert_list) init && cert_list "${@:2}" ;; cert_view) init && cert_view "${@:2}" ;; csr_create) init && csr_create "${@:2}" ;; csr_sign) init && csr_sign "${@:2}" ;; csr_sign_from_file) init && csr_sign_from_file "${@:2}" ;; csr_view) init && csr_view "${@:2}" ;; init) init "${@:2}" ;; version) version "${@:2}" ;; *) help ;; esac fi