From ef22701878bb10df567d60f2ac50dce52a82c9ee Mon Sep 17 00:00:00 2001 From: Richard T Bonhomme Date: Wed, 27 Apr 2022 19:27:34 +0100 Subject: [PATCH] Introduce 'revoke-renewed' When easyrsa "renews" a certificate, the current certificate is moved to a sub-directory for renewed certificates and renamed to the serial number of the certificate. This makes it difficult to subsequently revoke the old certificate. The new behaviour is for easyrsa to move the certificate without renaming the file. This means the certificate can be revoked by name. Once a renewed certificate is revoked, it is moved to the 'revoked' sub-directory, along with all other revoked certificates. The same mechanism also manages keys, requests, PKCS and inline files. Behaviour summary: * revoke moves certificates to 'revoked' - Unchanged Rename the certificate to its serial number - Unchanged * renew moves certificates to 'renewed' - Unchanged renew does not rename the certificate to its serial number - Changed Important: Only one certificate of a specific name (eg. john) can be renewed at the same time. To renew another certificate called 'john' the first MUST be revoked. * revoke-renewed: takes the certificate from 'renewed' - Changed moves the certifiate to 'revoked' - Changed renames the certificate to its serial number - Unchanged * All revoked certificates are moved to the 'revoked' sub-directory. Signed-off-by: Richard T Bonhomme --- easyrsa3/easyrsa | 439 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 310 insertions(+), 129 deletions(-) diff --git a/easyrsa3/easyrsa b/easyrsa3/easyrsa index be5272c..9fb7d04 100755 --- a/easyrsa3/easyrsa +++ b/easyrsa3/easyrsa @@ -34,6 +34,7 @@ Here is the list of commands available with a short syntax reminder. Use the build-client-full [ cmd-opts ] build-server-full [ cmd-opts ] revoke [cmd-opts] + revoke-renewed [cmd-opts] renew [cmd-opts] build-serverClient-full [ cmd-opts ] gen-crl @@ -113,6 +114,17 @@ cmd_help() { revoke) text=" revoke [reason] Revoke a certificate specified by the filename_base, with an optional + revocation reason that is one of: + unspecified + keyCompromise + CACompromise + affiliationChanged + superseded + cessationOfOperation + certificateHold";; + revoke-renewed) text=" + revoke-renewed [reason] + Revoke a renewed certificate specified by its old certificate_serial, with an optional revocation reason that is one of: unspecified keyCompromise @@ -827,7 +839,7 @@ install_data_to_pki () { # Note that A && B || C is not if-then-else. C may run when A is true # shellcheck disable=SC2015 [ -n "$EASYRSA_EXT_DIR" ] && [ -e "$EASYRSA_EXT_DIR" ] || \ - die "x509-types folder cannot be found" + die "x509-types folder cannot be found: $EASYRSA_EXT_DIR" # Complete or error [ -e "$EASYRSA_SAFE_CONF" ] || easyrsa_openssl makesafeconf @@ -910,8 +922,7 @@ current CA keypair. If you intended to start a new CA, run init-pki first." # create necessary files and dirs: err_file="Unable to create necessary PKI files (permissions?)" for i in issued certs_by_serial \ - revoked/certs_by_serial revoked/private_by_serial revoked/reqs_by_serial \ - renewed/certs_by_serial renewed/private_by_serial renewed/reqs_by_serial; + revoked/certs_by_serial revoked/private_by_serial revoked/reqs_by_serial; do mkdir -p "$EASYRSA_PKI/$i" || die "$err_file" done @@ -1392,7 +1403,12 @@ Run easyrsa without commands for usage and command help." # Assign file_name_base and dust off! file_name_base="$1" shift - crt_in="$EASYRSA_PKI/issued/$file_name_base.crt" + + in_dir="$EASYRSA_PKI" + crt_in="$in_dir/issued/$file_name_base.crt" + key_in="$in_dir/private/$file_name_base.key" + req_in="$in_dir/reqs/$file_name_base.req" + creds_in="$in_dir/$file_name_base.creds" # Assign possible "crl_reason" if [ "$1" ]; then @@ -1436,6 +1452,20 @@ $(display_dn x509 "$crt_in") Unable to revoke as the input file is not a valid certificate. Unexpected input in file: $crt_in" + # Verify request + if [ -e "$req_in" ] + then + verify_file req "$req_in" || die "\ +Unable to move request. The file is not a valid request. +Unexpected input in file: $req_in" + fi + + # get the serial number of the certificate -> serial=XXXX + cert_serial="$(easyrsa_openssl x509 -in "$crt_in" -noout -serial)" + # remove the serial= part -> we only need the XXXX part + cert_serial="${cert_serial##*=}" + duplicate_crt_by_serial="$EASYRSA_PKI/certs_by_serial/$cert_serial.pem" + # Revoke certificate easyrsa_openssl ca -utf8 -revoke "$crt_in" \ ${crl_reason+ -crl_reason "$crl_reason"} \ @@ -1443,7 +1473,7 @@ input in file: $crt_in" || die "Failed to revoke certificate: revocation command failed." # move revoked files so we can reissue certificates with the same name - move_revoked "$file_name_base" + move_revoked [ "$EASYRSA_SILENT" ] || print # Separate Notice below notice "\ @@ -1460,72 +1490,57 @@ infrastructure in order to prevent the revoked cert from being accepted." # moves revoked certificates to an alternative folder # allows reissuing certificates with the same name move_revoked() { - verify_ca_init + # Set out_dir + out_dir="$EASYRSA_PKI/revoked" + crt_out="$out_dir/certs_by_serial/$cert_serial.crt" + key_out="$out_dir/private_by_serial/$cert_serial.key" + req_out="$out_dir/reqs_by_serial/$cert_serial.req" - [ -n "$1" ] || die "\ -Error: didn't find a file base name as the first argument. -Run easyrsa without commands for usage and command help." - - crt_in="$EASYRSA_PKI/issued/$1.crt" - key_in="$EASYRSA_PKI/private/$1.key" - req_in="$EASYRSA_PKI/reqs/$1.req" - creds_in="$EASYRSA_PKI/$1.creds" - - verify_file x509 "$crt_in" || die "\ -Unable to move revoked input file. The file is not a valid certificate. -Unexpected input in file: $crt_in" - - if [ -e "$req_in" ] - then - verify_file req "$req_in" || die "\ -Unable to move request. The file is not a valid request. -Unexpected input in file: $req_in" - fi - - # get the serial number of the certificate -> serial=XXXX - cert_serial="$(easyrsa_openssl x509 -in "$crt_in" -noout -serial)" - # remove the serial= part -> we only need the XXXX part - cert_serial="${cert_serial##*=}" - - crt_by_serial="$EASYRSA_PKI/certs_by_serial/$cert_serial.pem" - crt_by_serial_revoked="$EASYRSA_PKI/revoked/certs_by_serial/$cert_serial.crt" - key_by_serial_revoked="$EASYRSA_PKI/revoked/private_by_serial/$cert_serial.key" - req_by_serial_revoked="$EASYRSA_PKI/revoked/reqs_by_serial/$cert_serial.req" + # NEVER over-write a revoked cert, serial number must be unique + [ -e "$crt_out" ] && die "revoked exists: $crt_out" + [ -e "$key_out" ] && die "revoked exists: $key_out" + [ -e "$req_out" ] && die "revoked exists: $req_out" # make sure revoked dirs exist - [ -d "$EASYRSA_PKI/revoked" ] || \ - mkdir "$EASYRSA_PKI/revoked" - [ -d "$EASYRSA_PKI/revoked/certs_by_serial" ] || \ - mkdir "$EASYRSA_PKI/revoked/certs_by_serial" - [ -d "$EASYRSA_PKI/revoked/private_by_serial" ] || \ - mkdir "$EASYRSA_PKI/revoked/private_by_serial" - [ -d "$EASYRSA_PKI/revoked/reqs_by_serial" ] || \ - mkdir "$EASYRSA_PKI/revoked/reqs_by_serial" + if [ ! -d "$out_dir" ]; then + mkdir -p "$out_dir" || die "Failed to mkdir: $out_dir" + fi + for target in certs_by_serial private_by_serial reqs_by_serial; do + [ -d "$out_dir/$target" ] && continue + mkdir -p "$out_dir/$target" \ + || die "Failed to mkdir: $out_dir/$target" + done - # move crt, key and req file to revoked folders - mv "$crt_in" "$crt_by_serial_revoked" - - # only move the req if we have it - [ -e "$req_in" ] && mv "$req_in" "$req_by_serial_revoked" + # move crt, key and req file to renewed_then_revoked folders + mv "$crt_in" "$crt_out" || die "Failed to move: $crt_in" # only move the key if we have it - [ -e "$key_in" ] && mv "$key_in" "$key_by_serial_revoked" + if [ -e "$key_in" ]; then + mv "$key_in" "$key_out" || die "Failed to move: $key_in" + fi - # move the rest of the files (p12, p7, ...) - for file in "$EASYRSA_PKI/private/$1"\.??? - do - # get file extension - file_ext="${file##*.}" + # only move the req if we have it + if [ -e "$req_in" ]; then + mv "$req_in" "$req_out" || die "Failed to move: $req_in" + fi - if [ -f "$file" ]; then - mv "$file" \ - "$EASYRSA_PKI/revoked/private_by_serial/$cert_serial.$file_ext" \ - || die "Failed to move file: $file" + # move any pkcs files + for pkcs in p12 p7b p8 p1; do + if [ -e "$in_dir/issued/$file_name_base.$pkcs" ]; then + mv "$in_dir/issued/$file_name_base.$pkcs" \ + "$out_dir/certs_by_serial/$cert_serial.$pkcs" \ + || die "Failed to move: $file_name_base.$pkcs" + elif [ -e "$in_dir/private/$file_name_base.$pkcs" ]; then + mv "$in_dir/private/$file_name_base.$pkcs" \ + "$out_dir/private_by_serial/$cert_serial.$pkcs" \ + || die "Failed to move: $file_name_base.$pkcs" + else + : # ok fi done # remove the duplicate certificate in the certs_by_serial folder - rm "$crt_by_serial" || warn \ + rm "$duplicate_crt_by_serial" || warn \ "Failed to remove the duplicate certificate in the certs_by_serial folder" # remove credentials file (if exists) @@ -1542,23 +1557,39 @@ renew() { verify_ca_init # pull filename base: - [ -n "$1" ] || die "\ + [ "$1" ] || die "\ Error: didn't find a file base name as the first argument. Run easyrsa without commands for usage and command help." - crt_in="$EASYRSA_PKI/issued/$1.crt" + + # Assign file_name_base and dust off! + file_name_base="$1" + shift + + in_dir="$EASYRSA_PKI" + crt_in="$in_dir/issued/$file_name_base.crt" + key_in="$in_dir/private/$file_name_base.key" + req_in="$in_dir/reqs/$file_name_base.req" + creds_in="$in_dir/$file_name_base.creds" # Upgrade CA index.txt.attr - unique_subject = no up23_upgrade_ca || die "Failed to upgrade CA to support renewal." - # Append 'nopass' + # Set 'nopass' opt_nopass="" - if [ "$2" ]; then - opt_nopass="$2" + if [ "$1" ]; then + opt_nopass="$1" + shift fi - verify_file x509 "$crt_in" || die "\ -Unable to renew as the input file is not a valid certificate. Unexpected -input in file: $crt_in" + # Enforce syntax + if [ "$1" ]; then + die "Syntax error: $1" + fi + + # referenced cert must exist: + [ -f "$crt_in" ] || die "\ +Unable to renew as no certificate was found. Certificate was expected +at: $crt_in" # confirm operation by displaying DN: confirm "Continue with renew: " "yes" " @@ -1567,15 +1598,29 @@ Please confirm you wish to renew the certificate with the following subject: $(display_dn x509 "$crt_in") " # => confirm end - # referenced cert must exist: - [ -f "$crt_in" ] || die "\ -Unable to renew as no certificate was found. Certificate was expected -at: $crt_in" + # Verify certificate + verify_file x509 "$crt_in" || die "\ +Unable to renew as the input file is not a valid certificate. Unexpected +input in file: $crt_in" + + # Verify request + if [ -e "$req_in" ] + then + verify_file req "$req_in" || die "\ +Unable to move request. The file is not a valid request. +Unexpected input in file: $req_in" + fi + + # get the serial number of the certificate -> serial=XXXX + cert_serial="$(easyrsa_openssl x509 -in "$crt_in" -noout -serial)" + # remove the serial= part -> we only need the XXXX part + cert_serial="${cert_serial##*=}" + duplicate_crt_by_serial="$EASYRSA_PKI/certs_by_serial/$cert_serial.pem" # Check if old cert is expired or expires within 30 cert_expire_date="$( - easyrsa_openssl x509 -in "$crt_in" -noout -enddate | - sed 's/^notAfter=//' + easyrsa_openssl x509 -in "$crt_in" -noout -enddate | \ + sed 's/^notAfter=//' )" case "$easyrsa_uname" in @@ -1638,11 +1683,10 @@ subjectAltName = $san" fi # move renewed files so we can reissue certificate with the same name - # FIXME: Modify revoke() to also work on the renewed certs subdir - move_renewed "$1" + move_renewed # renew certificate - build_full "$cert_type" "$1" "$opt_nopass" || die "\ + build_full "$cert_type" "$file_name_base" "$opt_nopass" || die "\ Failed to renew certificate: renew command failed." [ "$EASYRSA_SILENT" ] || print # Separate Notice below @@ -1660,72 +1704,57 @@ You may want to revoke the old certificate once the new one has been deployed." # moves renewed certificates to an alternative folder # allows reissuing certificates with the same name move_renewed() { - verify_ca_init + # Set out_dir + out_dir="$EASYRSA_PKI/renewed" + crt_out="$out_dir/issued/$file_name_base.crt" + key_out="$out_dir/private/$file_name_base.key" + req_out="$out_dir/reqs/$file_name_base.req" - [ -n "$1" ] || die "\ -Error: didn't find a file base name as the first argument. -Run easyrsa without commands for usage and command help." - - crt_in="$EASYRSA_PKI/issued/$1.crt" - key_in="$EASYRSA_PKI/private/$1.key" - req_in="$EASYRSA_PKI/reqs/$1.req" - creds_in="$EASYRSA_PKI/$1.creds" - - verify_file x509 "$crt_in" || die "\ -Unable to move renewed input file. The file is not a valid certificate. -Unexpected input in file: $crt_in" - - if [ -e "$req_in" ] - then - verify_file req "$req_in" || die "\ -Unable to move request. The file is not a valid request. -Unexpected input in file: $req_in" - fi - - # get the serial number of the certificate -> serial=XXXX - cert_serial="$(easyrsa_openssl x509 -in "$crt_in" -noout -serial)" - # remove the serial= part -> we only need the XXXX part - cert_serial="${cert_serial##*=}" - - crt_by_serial="$EASYRSA_PKI/certs_by_serial/$cert_serial.pem" - crt_by_serial_renewed="$EASYRSA_PKI/renewed/certs_by_serial/$cert_serial.crt" - key_by_serial_renewed="$EASYRSA_PKI/renewed/private_by_serial/$cert_serial.key" - req_by_serial_renewed="$EASYRSA_PKI/renewed/reqs_by_serial/$cert_serial.req" + # NEVER over-write a renewed cert, revoke it first + [ -e "$crt_out" ] && die "renewed exists: $crt_out" + [ -e "$key_out" ] && die "renewed exists: $key_out" + [ -e "$req_out" ] && die "renewed exists: $req_out" # make sure renewed dirs exist - [ -d "$EASYRSA_PKI/renewed" ] || \ - mkdir "$EASYRSA_PKI/renewed" - [ -d "$EASYRSA_PKI/renewed/certs_by_serial" ] || \ - mkdir "$EASYRSA_PKI/renewed/certs_by_serial" - [ -d "$EASYRSA_PKI/renewed/private_by_serial" ] || \ - mkdir "$EASYRSA_PKI/renewed/private_by_serial" - [ -d "$EASYRSA_PKI/renewed/reqs_by_serial" ] || \ - mkdir "$EASYRSA_PKI/renewed/reqs_by_serial" + if [ ! -d "$out_dir" ]; then + mkdir -p "$out_dir" || die "Failed to mkdir: $out_dir" + fi + for target in issued private reqs; do + [ -d "$out_dir/$target" ] && continue + mkdir -p "$out_dir/$target" \ + || die "Failed to mkdir: $out_dir/$target" + done # move crt, key and req file to renewed folders - mv "$crt_in" "$crt_by_serial_renewed" - - # only move the req if we have it - [ -e "$req_in" ] && mv "$req_in" "$req_by_serial_renewed" + mv "$crt_in" "$crt_out" || die "Failed to move: $crt_in" # only move the key if we have it - [ -e "$key_in" ] && mv "$key_in" "$key_by_serial_renewed" + if [ -e "$key_in" ]; then + mv "$key_in" "$key_out" || die "Failed to move: $key_in" + fi - # move the rest of the files (p12, p7, ...) - for file in "$EASYRSA_PKI/private/$1"\.??? - do - # get file extension - file_ext="${file##*.}" + # only move the req if we have it + if [ -e "$req_in" ]; then + mv "$req_in" "$req_out" || die "Failed to move: $req_in" + fi - if [ -f "$file" ]; then - mv "$file" \ - "$EASYRSA_PKI/renewed/private_by_serial/$cert_serial.$file_ext" \ - || die "Failed to move file: $file" + # move any pkcs files + for pkcs in p12 p7b p8 p1; do + if [ -e "$in_dir/issued/$file_name_base.$pkcs" ]; then + mv "$in_dir/issued/$file_name_base.$pkcs" \ + "$out_dir/issued/$file_name_base.$pkcs" \ + || die "Failed to move: $file_name_base.$pkcs" + elif [ -e "$in_dir/private/$file_name_base.$pkcs" ]; then + mv "$in_dir/private/$file_name_base.$pkcs" \ + "$out_dir/private/$file_name_base.$pkcs" \ + || die "Failed to move: $file_name_base.$pkcs" + else + : # ok fi done # remove the duplicate certificate in the certs_by_serial folder - rm "$crt_by_serial" || warn \ + rm "$duplicate_crt_by_serial" || warn \ "Failed to remove the duplicate certificate in the certs_by_serial folder" # remove credentials file (if exists) @@ -1737,6 +1766,155 @@ Unexpected input in file: $req_in" return 0 } #= move_renewed() +# revoke-renewed backend +revoke_renewed() { + verify_ca_init + + # pull filename base: + [ "$1" ] || die "\ +Error: didn't find a file base name as the first argument. +Run easyrsa without commands for usage and command help." + + # Assign file_name_base and dust off! + file_name_base="$1" + shift + + in_dir="$EASYRSA_PKI/renewed" + crt_in="$in_dir/issued/$file_name_base.crt" + key_in="$in_dir/private/$file_name_base.key" + req_in="$in_dir/reqs/$file_name_base.req" + #creds_in="$EASYRSA_PKI/$file_name_base.creds" + + # Assign possible "crl_reason" + if [ "$1" ]; then + crl_reason="$1" + shift + + case "$crl_reason" in + unspecified | \ + keyCompromise |\ + CACompromise | \ + affiliationChanged | \ + superseded | \ + cessationOfOperation | \ + certificateHold ) : # ok + ;; + *) die "Illegal reason: $crl_reason" + esac + else + unset -v crl_reason + fi + + # Enforce syntax + if [ "$1" ]; then + die "Syntax error: $1" + fi + + # referenced cert must exist: + [ -f "$crt_in" ] || die "\ +Unable to revoke as no renewed certificate was found. Certificate was expected +at: $crt_in" + + # confirm operation by displaying DN: + confirm "Continue with revocation: " "yes" " +Please confirm you wish to revoke the renewed certificate with serial $1 and the following subject: + +$(display_dn x509 "$crt_in") +" + + # Verify certificate + verify_file x509 "$crt_in" || die "\ +Unable to revoke as the certificate serial does not match an old one of a valid renewed certificate. Unexpected +certificate in file: $crt_in" + + # Verify request + if [ -e "$req_in" ] + then + verify_file req "$req_in" || die "\ +Unable to move renewed then revoked request. The file is not a valid request. Unexpected +input in file: $req_in" + fi + + # get the serial number of the certificate -> serial=XXXX + cert_serial="$(easyrsa_openssl x509 -in "$crt_in" -noout -serial)" \ + || die "renew-revoked - Failed to retrieve certificate serial number" + # remove the serial= part -> we only need the XXXX part + cert_serial="${cert_serial##*=}" + + # shellcheck disable=SC2086 + easyrsa_openssl ca -utf8 -revoke "$crt_in" \ + ${crl_reason:+ -crl_reason "$crl_reason"} \ + ${EASYRSA_PASSIN:+ -passin "$EASYRSA_PASSIN"} \ + || die "Failed to revoke renewed certificate: revocation command failed." + + # move revoked files + move_renewed_revoked + + notice "\ +IMPORTANT!!! + +Revocation was successful. You must run gen-crl and upload a CRL to your +infrastructure in order to prevent the revoked renewed cert from being accepted. +" # => notice end + + return 0 +} #= revoke_renewed() + +# move-renewed-revoked +# moves renewed then revoked certificates to an alternative folder +move_renewed_revoked() { + # output + out_dir="$EASYRSA_PKI/revoked" + crt_out="$out_dir/certs_by_serial/$cert_serial.crt" + key_out="$out_dir/private_by_serial/$cert_serial.key" + req_out="$out_dir/reqs_by_serial/$cert_serial.req" + + # NEVER over-write a revoked cert, serial number must be unique + [ -e "$crt_out" ] && die "revoked exists: $crt_out" + [ -e "$key_out" ] && die "revoked exists: $key_out" + [ -e "$req_out" ] && die "revoked exists: $req_out" + + # make sure revoked dirs exist + if [ ! -d "$out_dir" ]; then + mkdir -p "$out_dir" || die "move_renewed_revoked - Failed to mkdir: $out_dir" + fi + for target in certs_by_serial private_by_serial reqs_by_serial; do + [ -d "$out_dir/$target" ] && continue + mkdir -p "$out_dir/$target" \ + || die "Failed to mkdir: $out_dir/$target" + done + + # move crt, key and req file to renewed_then_revoked folders + mv "$crt_in" "$crt_out" || die "Failed to move: $crt_in" + + # only move the key if we have it + if [ -e "$key_in" ]; then + mv "$key_in" "$key_out" || die "Failed to move: $key_in" + fi + + # only move the req if we have it + if [ -e "$req_in" ]; then + mv "$req_in" "$req_out" || die "Failed to move: $req_in" + fi + + # move any pkcs files + for pkcs in p12 p7b p8 p1; do + if [ -e "$in_dir/issued/$file_name_base.$pkcs" ]; then + mv "$in_dir/issued/$file_name_base.$pkcs" \ + "$out_dir/certs_by_serial/$cert_serial.$pkcs" \ + || die "Failed to move: $file_name_base.$pkcs" + elif [ -e "$in_dir/private/$file_name_base.$pkcs" ]; then + mv "$in_dir/private/$file_name_base.$pkcs" \ + "$out_dir/private_by_serial/$cert_serial.$pkcs" \ + || die "Failed to move: $file_name_base.$pkcs" + else + : # ok + fi + done + + return 0 +} # => move_renewed_revoked() + # gen-crl backend gen_crl() { verify_ca_init @@ -3165,6 +3343,9 @@ case "$cmd" in revoke) revoke "$@" ;; + revoke-renewed) + revoke_renewed "$@" + ;; renew) renew "$@" ;;