From 6317779ff9c3192dc8579415f800249a7335a6ad Mon Sep 17 00:00:00 2001 From: Richard T Bonhomme Date: Tue, 13 Sep 2022 13:54:11 +0100 Subject: [PATCH] Rename 'renew' to 'rebuild' - Introduce 'renew' version 3 The old 'renew' always created a new private key at the CA signing side. This forces the entity private key to be exposed, unnecessarily. Only certificates expire, therefore, only certificates can be renewed. For those reasons, the old 'renew' has been renamed to 'rebuild'. The new 'renew' creates a new certificate only from the existing CSR. The existing CSR will always exist, allowing a certificate to be renewed indefinitely. Both 'rebuild' and 'renew' use the renewed/ folders to temporarily store the old certificate, where it can be revoked with 'revoke-renewed' command. Manually tested. Closes: #684 Notes: * renew version 1 did not have a command to revoke old certificates. * renew version 2 allowed old certificates to be revoked via command 'revoke-renewed'. * renew version 3 only renews the certificate. Signed-off-by: Richard T Bonhomme --- easyrsa3/easyrsa | 368 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 304 insertions(+), 64 deletions(-) diff --git a/easyrsa3/easyrsa b/easyrsa3/easyrsa index ecb876c23..c0b6af03a 100755 --- a/easyrsa3/easyrsa +++ b/easyrsa3/easyrsa @@ -40,6 +40,7 @@ Here is the list of commands available with a short syntax reminder. Use the renewable [ ] revoke-renewed [cmd-opts] rewind-renew + rebuild [cmd-opts] gen-crl update-db show-req [ cmd-opts ] @@ -172,12 +173,21 @@ cmd_help() { superseded cessationOfOperation certificateHold" + ;; + rebuild) + text=" +* rebuild [ cmd-opts ] + + Rebuild a certificate and key specified by " + + opts=" + * nopass - do not encrypt the private key (default is encrypted)" ;; renew) text=" * renew [ cmd-opts ] - Renew a certificate specified by the " + Renew a certificate specified by " opts=" * nopass - do not encrypt the private key (default is encrypted)" @@ -188,7 +198,7 @@ cmd_help() { Check which certificates can be renewed" ;; - rewind-renew) + rewind|rewind-renew) text=" * rewind-renew @@ -673,9 +683,9 @@ cleanup() { fi # Restore files when renew is interrupted - if [ "$on_error_undo_renew_move" ]; then - renew_restore_move - fi + [ "$on_error_undo_renew_move" ] && renew_restore_move; : + # Restore files when rebuild is interrupted + [ "$on_error_undo_rebuild_move" ] && rebuild_restore_move; : # shellcheck disable=SC3040 # In POSIX sh, set option [name] is undefined case "$easyrsa_host_os" in @@ -1930,13 +1940,16 @@ Cannot revoke this certificate because a conflicting file exists. unset -v deny_msg # confirm operation by displaying DN: + unset -v if_exist_key_in if_exist_req_in + [ -e "$key_in" ] && if_exist_key_in=" +* $key_in" + [ -e "$req_in" ] && if_exist_req_in=" +* $req_in" warn "\ This process is destructive! These files will be moved to the 'revoked' storage sub-directory: -* $crt_in -* $key_in -* $req_in +* $crt_in${if_exist_key_in}${if_exist_req_in} These files will be DELETED: * All PKCS files for commonName : $file_name_base @@ -2044,7 +2057,6 @@ Run easyrsa without commands for usage and command help." 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" @@ -2085,25 +2097,14 @@ Unexpected input in file: $req_in" # 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" # NEVER over-write a renewed cert, revoke it first deny_msg="\ Cannot renew this certificate because a conflicting file exists. *" [ -e "$crt_out" ] && die "$deny_msg certificate: $crt_out" - [ -e "$key_out" ] && die "$deny_msg private key: $key_out" - [ -e "$req_out" ] && die "$deny_msg request : $req_out" unset -v deny_msg - # # Check if old cert is expired or expires within 30 - # cert_dates "$crt_in" - # - # [ "$expire_date_s" -lt "$allow_renew_date_s" ] || die "\ - #Certificate expires in more than $EASYRSA_CERT_RENEW days. - #Renewal not allowed." - # Extract certificate usage from old cert cert_ext_key_usage="$( easyrsa_openssl x509 -in "$crt_in" -noout -text | @@ -2143,8 +2144,6 @@ This process is destructive! These files will be moved to the 'renewed' storage sub-directory: * $crt_in -* $key_in -* $req_in These files will be DELETED: * All PKCS files for commonName : $file_name_base @@ -2166,11 +2165,10 @@ with the following subject: on_error_undo_renew_move=1 # renew certificate - if build_full "$cert_type" "$file_name_base" "$opt_nopass"; then + if EASYRSA_BATCH=1 sign_req "$cert_type" "$file_name_base"; then unset on_error_undo_renew_move else - # If renew failed then restore cert, key and req. Otherwise, issue a warning - # If *restore* fails then at least the file-names are not serial-numbers + # If renew failed then restore cert. Otherwise, issue a warning renew_restore_move die "\ Renewal has failed to build a new certificate/key pair." @@ -2181,7 +2179,7 @@ Renewal has failed to build a new certificate/key pair." * IMPORTANT * -Renew has created a new certificate and key, both files MUST be replaced! +Renew has created a new certificate, to replace the old certificate. To revoke the old certificate, once the new one has been deployed, use: 'revoke-renewed $file_name_base reason' ('reason' is optional)" @@ -2192,7 +2190,7 @@ use: 'revoke-renewed $file_name_base reason' ('reason' is optional)" # Restore files on failure to renew renew_restore_move() { unset -v rrm_err on_error_undo_renew_move - # restore crt, key and req file to PKI folders + # restore crt file to PKI folders if mv "$restore_crt_out" "$restore_crt_in"; then : # ok else @@ -2200,26 +2198,6 @@ renew_restore_move() { rrm_err=1 fi - # only restore the key if we have it - if [ -e "$restore_key_out" ]; then - if mv "$restore_key_out" "$restore_key_in"; then - : # ok - else - warn "Failed to restore: $restore_key_out" - rrm_err=1 - fi - fi - - # only restore the req if we have it - if [ -e "$restore_req_out" ]; then - if mv "$restore_req_out" "$restore_req_in"; then - : # ok - else - warn "Failed to restore: $restore_req_out" - rrm_err=1 - fi - fi - # messages if [ "$rrm_err" ]; then warn "Failed to restore renewed files." @@ -2250,20 +2228,6 @@ renew_move() { restore_crt_out="$crt_out" mv "$crt_in" "$crt_out" || die "Failed to move: $crt_in" - # only move the key if we have it - restore_key_in="$key_in" - restore_key_out="$key_out" - if [ -e "$key_in" ]; then - mv "$key_in" "$key_out" || warn "Failed to move: $key_in" - fi - - # only move the req if we have it - restore_req_in="$req_in" - restore_req_out="$req_out" - if [ -e "$req_in" ]; then - mv "$req_in" "$req_out" || warn "Failed to move: $req_in" - fi - # remove any pkcs files for pkcs in p12 p7b p8 p1; do if [ -e "$in_dir/issued/$file_name_base.$pkcs" ]; then @@ -2377,13 +2341,16 @@ Cannot revoke this certificate because a conflicting file exists. unset -v deny_msg # confirm operation by displaying DN: + unset -v if_exist_key_in if_exist_req_in + [ -e "$key_in" ] && if_exist_key_in=" +* $key_in" + [ -e "$req_in" ] && if_exist_req_in=" +* $req_in" warn "\ This process is destructive! These files will be moved to the 'revoked' storage sub-directory: -* $crt_in -* $key_in -* $req_in" +* $crt_in${if_exist_key_in}${if_exist_req_in}" confirm " Continue with revocation: " "yes" "\ Please confirm you wish to revoke the renewed certificate @@ -2594,6 +2561,275 @@ Serial number: $cert_serial To revoke use: 'revoke-renewed $crt_cn'" } # => rewind_renew() +# rebuild backend +rebuild() { + # 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." + + verify_ca_init + + # 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." + + # Set 'nopass' + unset -v opt_nopass + case "$1" in + nopass) opt_nopass="$1"; shift ;; + '') : ;; # Empty ok + *) die "Unknown option: $1" + esac + + # referenced cert must exist: + [ -f "$crt_in" ] || die "\ +Unable to rebuild as no certificate was found. Certificate was expected +at: $crt_in" + + # Verify certificate + verify_file x509 "$crt_in" || die "\ +Unable to rebuild 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 verify 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" + + # 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" + + # NEVER over-write a renewed cert, revoke it first + deny_msg="\ +Cannot rebuild this certificate because a conflicting file exists. +*" + [ -e "$crt_out" ] && die "$deny_msg certificate: $crt_out" + [ -e "$key_out" ] && die "$deny_msg private key: $key_out" + [ -e "$req_out" ] && die "$deny_msg request : $req_out" + unset -v deny_msg + + # # Check if old cert is expired or expires within 30 + # cert_dates "$crt_in" + # + # [ "$expire_date_s" -lt "$allow_renew_date_s" ] || die "\ + #Certificate expires in more than $EASYRSA_CERT_RENEW days. + #Renewal not allowed." + + # Extract certificate usage from old cert + cert_ext_key_usage="$( + easyrsa_openssl x509 -in "$crt_in" -noout -text | + sed -n "/X509v3 Extended Key Usage:/{n;s/^ *//g;p;}" + )" + + case "$cert_ext_key_usage" in + "TLS Web Client Authentication") + cert_type=client + ;; + "TLS Web Server Authentication") + cert_type=server + ;; + "TLS Web Server Authentication, TLS Web Client Authentication") + cert_type=serverClient + ;; + *) die "Unknown key usage: $cert_ext_key_usage" + esac + + # Use SAN from --subject-alt-name if set else use SAN from old cert + if echo "$EASYRSA_EXTRA_EXTS" | grep -q subjectAltName; then + : # ok - Use current subjectAltName + else + san="$( + easyrsa_openssl x509 -in "$crt_in" -noout -text | sed -n \ + "/X509v3 Subject Alternative Name:/{n;s/IP Address:/IP:/g;s/ //g;p;}" + )" + + [ "$san" ] && export EASYRSA_EXTRA_EXTS="\ +$EASYRSA_EXTRA_EXTS +subjectAltName = $san" + fi + + # confirm operation by displaying DN: + unset -v if_exist_key_in if_exist_req_in + [ -e "$key_in" ] && if_exist_key_in=" +* $key_in" + [ -e "$req_in" ] && if_exist_req_in=" +* $req_in" + warn "\ +This process is destructive! + +These files will be moved to the 'renewed' storage sub-directory: +* $crt_in${if_exist_key_in}${if_exist_req_in} + +These files will be DELETED: +* All PKCS files for commonName : $file_name_base +* The inline credentials file : $creds_in +* The duplicate certificate : $duplicate_crt_by_serial + +IMPORTANT: The new key will${opt_nopass+ NOT} be password protected." + + confirm " Continue with rebuild: " "yes" "\ +Please confirm you wish to renew the certificate +with the following subject: + + $(display_dn x509 "$crt_in") + + serial-number: $cert_serial" + + # move renewed files so we can reissue certificate with the same name + rebuild_move + on_error_undo_rebuild_move=1 + + # rebuild certificate + if EASYRSA_BATCH=1 build_full "$cert_type" "$file_name_base" "$opt_nopass"; then + unset on_error_undo_rebuild_move + else + # If rebuild failed then restore cert, key and req. Otherwise, + # issue a warning. If *restore* fails then at least the file-names + # are not serial-numbers + rebuild_restore_move + die "\ +Rebuild has failed to build a new certificate/key pair." + fi + + # Success messages + notice "Rebuild was successful. + + * IMPORTANT * + +Rebuild has created a new certificate and key, to replace both old files. + +To revoke the old certificate, once the new one has been deployed, +use: 'revoke-renewed $file_name_base reason' ('reason' is optional)" + + return 0 +} # => rebuild() + +# Restore files on failure to rebuild +rebuild_restore_move() { + unset -v rrm_err on_error_undo_renew_move + # restore crt, key and req file to PKI folders + if mv "$restore_crt_out" "$restore_crt_in"; then + : # ok + else + warn "Failed to restore: $restore_crt_out" + rrm_err=1 + fi + + # only restore the key if we have it + if [ -e "$restore_key_out" ]; then + if mv "$restore_key_out" "$restore_key_in"; then + : # ok + else + warn "Failed to restore: $restore_key_out" + rrm_err=1 + fi + fi + + # only restore the req if we have it + if [ -e "$restore_req_out" ]; then + if mv "$restore_req_out" "$restore_req_in"; then + : # ok + else + warn "Failed to restore: $restore_req_out" + rrm_err=1 + fi + fi + + # messages + if [ "$rrm_err" ]; then + warn "Failed to restore renewed files." + else + notice "Rebuild FAILED but files have been successfully restored." + fi + + return 0 +} # => rebuild_restore_move() + +# rebuild_move +# moves renewed certificates to the 'renewed' folder +# allows reissuing certificates with the same name +rebuild_move() { + # make sure renewed dirs exist + for target in "$out_dir" \ + "$out_dir/issued" \ + "$out_dir/private" \ + "$out_dir/reqs" + do + [ -d "$target" ] && continue + mkdir -p "$target" || + die "Failed to mkdir: $target" + done + + # move crt, key and req file to renewed folders + restore_crt_in="$crt_in" + restore_crt_out="$crt_out" + mv "$crt_in" "$crt_out" || die "Failed to move: $crt_in" + + # only move the key if we have it + restore_key_in="$key_in" + restore_key_out="$key_out" + if [ -e "$key_in" ]; then + mv "$key_in" "$key_out" || warn "Failed to move: $key_in" + fi + + # only move the req if we have it + restore_req_in="$req_in" + restore_req_out="$req_out" + if [ -e "$req_in" ]; then + mv "$req_in" "$req_out" || warn "Failed to move: $req_in" + fi + + # remove any pkcs files + for pkcs in p12 p7b p8 p1; do + if [ -e "$in_dir/issued/$file_name_base.$pkcs" ]; then + # issued + rm "$in_dir/issued/$file_name_base.$pkcs" || + warn "Failed to remove: $file_name_base.$pkcs" + + elif [ -e "$in_dir/private/$file_name_base.$pkcs" ]; then + # private + rm "$in_dir/private/$file_name_base.$pkcs" || + warn "Failed to remove: $file_name_base.$pkcs" + else + : # ok + fi + done + + # remove the duplicate certificate in the certs_by_serial folder + if [ -e "$duplicate_crt_by_serial" ]; then + rm "$duplicate_crt_by_serial" || warn "\ +Failed to remove the duplicate certificate in the certs_by_serial folder" + fi + + # remove credentials file (if exists) + if [ -e "$creds_in" ]; then + rm "$creds_in" || warn "Failed to remove the inline file." + fi + + return 0 +} # => rebuild_move() + # gen-crl backend gen_crl() { verify_ca_init @@ -4822,6 +5058,10 @@ case "$cmd" in rewind-renew) rewind_renew "$@" ;; + rebuild) + [ "$alias_days" ] && export EASYRSA_CERT_EXPIRE="$alias_days"; : + rebuild "$@" + ;; import-req) import_req "$@" ;;