Skip to content

Instantly share code, notes, and snippets.

@duzun
Last active August 26, 2024 22:28
Show Gist options
  • Save duzun/064e068d6540e08753d4162d97f8f24e to your computer and use it in GitHub Desktop.
Save duzun/064e068d6540e08753d4162d97f8f24e to your computer and use it in GitHub Desktop.
Create a SSL certificate signed by your CA root certificate.
-----BEGIN CERTIFICATE-----
MIIEGTCCAwGgAwIBAgIUZyZoolDHKeOs73wadSEAe5AbuS8wDQYJKoZIhvcNAQEL
BQAwgYQxFjAUBgNVBAMMDXJvb3QuRFV6dW4uTWUxCzAJBgNVBAYTAlVTMRMwEQYD
VQQIDApDYWxpZm9ybmlhMRQwEgYDVQQHDAtMb3MgQW5nZWxlczERMA8GA1UECgwI
RFV6dW4uTWUxHzAdBgkqhkiG9w0BCQEWEGNvbnRhY3RAZHV6dW4ubWUwHhcNMjMw
OTA3MjMyODA1WhcNMzMwOTA3MjMyODA1WjCBhDEWMBQGA1UEAwwNcm9vdC5EVXp1
bi5NZTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFDASBgNVBAcM
C0xvcyBBbmdlbGVzMREwDwYDVQQKDAhEVXp1bi5NZTEfMB0GCSqGSIb3DQEJARYQ
Y29udGFjdEBkdXp1bi5tZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AKOL2/MGGodYoyp2AUKhuwjDoQrDii4qZnwGZkx5AD/QajGLhhAKp1fwTmo43xED
XsFyYTd4dnwsqRJdA6t9MY6+9lFZB9DrppndVQKta1Cf/4Wt6acAzJHrFXLoYjtV
M09/e4k15m+iwpsMkjPWwZzQ4tjnBOiu6w6b557Df3xu6shQ5apEkGGuHOr/bxoO
35H7X7zQjHJHhIRLk1b7l2ElLogw9sY9jz0UlNeiZn78WjJmOhIwebtCWFSNqaQD
RtdFupWNHF+o12KBj2I6HUEif3di4+O4rg8mhi1wsNk5pfOipYjHe+YqJMydVlNl
HIt+hIzVkkIQlBJvIATm6YUCAwEAAaOBgDB+MB0GA1UdDgQWBBTI8kGTp2yan6u8
3ZM9gs7jljnSGzAfBgNVHSMEGDAWgBTI8kGTp2yan6u83ZM9gs7jljnSGzAPBgNV
HRMBAf8EBTADAQH/MCsGA1UdEQQkMCKCCERVenVuLk1lggoqLkRVenVuLk1lggQq
LmxohwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQBptS+swThBn85LIVFdmnEAXgGJ
qUTO55VQLDHGAT4p5thvXlvVxP+jI2KM/46dU3qyrmFZotFDLW6pcLJxpD1/7/AY
cC/TwasJvOM6Zfd/DJ3LEswwQfYGj1KVSshX+AHHM2JvAcf2wzqiGdazEFMSgDir
8uTwTwXz8IMC0QhVd3RZXX//qQ6KS9FlwTbLhZdz28fxuMpldVB1L6+VLhEpcnWZ
qv58j/aWAMz+tSX0WRhj/dFb5ImTAYRU2YPw6mHo/99sMJv4czKjDv9nKWNsrmXK
0JVw+cCyiEeXfR1DmBY/kXKIPHXtE7sV5GN+hAVbcmgwM+fD2WhaYEYrlFcD
-----END CERTIFICATE-----
#!/usr/bin/env bash
##
# Generate a signed certificate for a csr or key using my root_ca.
#
# Note:
# $ROOT_CA_CERT must be in the trusted root ca (see https://www.archlinux.org/news/ca-certificates-update/)
#
# If you use Firefox as a browser you will need to import the public ca.crt certificate into Firefox directly.
# Firefox does not use the local operating system’s certificate store:
# https://support.mozilla.org/en-US/kb/setting-certificate-authorities-firefox
#
# If you are using your CA to integrate with a Windows environment or desktop computers, please see the documentation on how to use certutil.exe:
# https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/certutil#-installcert
#
# @author Dumitru Uzun (DUzun.Me)
##
# https://scriptcrunch.com/create-ca-tls-ssl-certificates-keys/
VERSION=1.3.0
_me_=$(basename "$0")
_dir_=$(realpath "$(dirname "$0")")
A_action=sign
F_dry_run=
F_years=5
F_keylen=4096
F_ca_years=10
F_ca_key=
F_ca_cert=
[ -z "$ROOT_CA" ] && ROOT_CA=duzun_root_CA
[ -z "$ROOT_CA_CERT" ] && ROOT_CA_CERT=ca/${ROOT_CA}.crt
[ -z "$ROOT_CA_KEY" ] && ROOT_CA_KEY=ca/${ROOT_CA}.key
# Where to look for root CA cert & key
ROOT_CA_DIRS=(
"$PWD"
"${HOME}/.ssh"
"$(realpath -m "${_dir_}/../lib/${_me_%.*}")"
"${_dir_}"
/root/.ssh
)
usage() {
cat <<EOS
Syntax:
$_me_ <action> [OPTIONS] <argument>
Usage:
$_me_ [sign] [OPTIONS] <name>.csr | <name>.key
$_me_ ca [OPTIONS] <domain>
$_me_ [-]h | [--]help | [-]v | [--]version
OPTIONS:
-d | --dry_run
-y=<years> | --years=<years> of validity, default to 5
-l=<len> | --keylen=<len> in bits, default to 4096
--ca_key=<path/to/root_ca.key> default to "$ROOT_CA_KEY"
--ca_crt=<path/to/root_ca.crt> default to "$ROOT_CA_CERT"
sign
Sign/create a new <name>.pem certificate using a CSR and/or key.
If <name>.key is used, a new <name>.csr is generated from prompts.
If <name>.csr is used, no need to input the fields.
If there is a <name>.ext file, it is used with -extfile,
otherwise it is generated to be used the next time.
ca
Sign/create a CA ceritificate.
version | v
Show $VERSION
help | h
Show this message
EOS
}
main() {
local -a _argv
local i
local -a d=()
local -A D=()
for i in "${ROOT_CA_DIRS[@]}"; do
if [[ -n "$i" ]]; then
[[ -z "${D["$i"]}" ]] && d+=("$i")
D["$i"]=1
fi
done
ROOT_CA_DIRS=("${d[@]}")
process_argv() {
local -a opts exit
# Detect action
case $1 in
sign | ca | pem2ext)
A_action="$1"
shift
;;
v | version)
A_action='version'
exit=1
;;
h | help | '')
A_action='usage'
exit=1
;;
esac
# Split options & arguments
while [[ $# -gt 0 ]]; do
[[ -n "$exit" ]] && return
case $1 in
-h | --help)
A_action='usage'
exit=1
;;
-v | --version)
A_action='version'
exit=1
;;
-a | --ca)
opts=(-a "${opts[@]}")
;;
--)
shift
_argv+=("$@")
break
;;
-*)
opts+=("$1")
;;
*)
_argv+=("$1")
;;
esac
shift
done
# Read options to flags
set -- "${opts[@]}"
while [[ $# -gt 0 ]]; do
case $1 in
-d | --dry_run) F_dry_run=1 ;;
--ca_years=*)
F_ca_years="${1#*=}"
;;
-y=* | --years=*)
if [[ "$A_action" = "ca" ]]; then
F_ca_years="${1#*=}"
else
F_years="${1#*=}"
fi
;;
-l=* | --keylen=*)
F_keylen="${1#*=}"
;;
esac
shift
done
}
process_argv "$@"
$A_action "${_argv[@]}"
return $?
}
sign() {
local root_ca root_ca_key
local i f kl fn
local key keypath dir
local basename ext name
local -a arg dns
local error
fn="${F_ca_cert:-$ROOT_CA_CERT}"
if ! root_ca="$(find_ca "$fn")"; then
echo >&2 "$fn not found"
error=4
fi
fn="${F_ca_key:-$ROOT_CA_KEY}"
if ! root_ca_key="$(find_ca "$fn")"; then
echo >&2 "$fn not found"
error=5
fi
[[ -n "$error" ]] && return "$error"
key=$1
dir=$(dirname "$key")
basename="$(basename "$key")"
ext="${basename##*.}"
name="${basename%.*}"
keypath="$(realpath "$1")"
if [[ "$dir" = "." ]]; then
dir="$(dirname "$keypath")"
else
dir="$(realpath "$dir")"
fi
case $ext in
'csr' | 'key') ;;
'' | 'ext' | 'pem' | 'crt' | "$key") ext= ;;
*)
name="$name.$ext"
ext=
;;
esac
if [ -z "$ext" ]; then
key="$dir/$name.csr" && [ -f "$key" ] ||
key="$dir/$name.key" && [ -f "$key" ] ||
(usage >&2 && return 2)
ext="${key##*.}"
elif [ ! -f "$key" ]; then
echo >&2 "File \"$key\" not found"
if choice "Generate a new key file"; then
# kl=$(default "Key length" 4096)
# openssl genrsa -out "$key" "$kl" || return $?
:
else
return 3
fi
fi
echo "key: $key"
echo "dir: $dir"
# Generate CSR from .key
if [[ "$ext" = "key" ]]; then
arg=("-key" "$key")
if [ ! -f "$key" ]; then
kl=$(default "Key length" 4096)
arg=("-newkey" "rsa:$kl" "-keyout" "$key")
fi
arg=(req -new -sha256 -nodes -out "$dir/$name.csr" "${arg[@]}")
if [ -f "$dir/$name.ext" ]; then
openssl "${arg[@]}" -config <(cat "$dir/$name.ext")
else
openssl "${arg[@]}"
fi
key="$dir/$name.csr"
ext="${key##*.}"
fi
# Generate .ext file if missing
if [ ! -f "$dir/$name.ext" ]; then
mapfile -t dns < <(_detect_DNSes "$dir" "$name")
gen_ext_cnt "${dns[@]}" >"$dir/$name.ext"
fi
arg=(
x509 -req -in "$key"
-CA "$root_ca"
-CAkey "$root_ca_key"
-CAcreateserial
-out "$dir/$name.pem"
-days "$((F_years * 365 + 1))"
-sha256
)
if [ -f "$dir/$name.ext" ]; then
arg+=(-extfile "$dir/$name.ext")
norm_ext_file "$dir/$name.ext"
fi
if [ -n "$F_dry_run" ]; then
echo openssl "${arg[@]}"
else
openssl "${arg[@]}"
if [ -f "$dir/$name.pem" ]; then
cat "$root_ca" >>"$dir/$name.pem"
fi
fi
}
version() {
echo "$VERSION"
}
ca() {
local domain=${1:-DUzun.Me}
local fn dn trust_cmd
dn=$(echo "${domain,,}" | sed -r 's/\.\w+$//' | tr . _)_root_CA
local root_ca_key root_ca_cert
fn="${F_ca_key:-$dn.key}"
if ! root_ca_key="$(find_ca "$fn")"; then
echo >&2 "No key file $fn"
if choice "Generate a new key file"; then
openssl genrsa -out "$fn" "$F_keylen" || return $?
[[ -z $root_ca_key ]] && root_ca_key=$fn
choice "Save a copy of \"$fn\" to \"$HOME/.ssh/\"" &&
cp -- "$fn" "$HOME/.ssh/"
else
return 1
fi
fi
openssl req -x509 -noenc \
-newkey rsa:"$F_keylen" \
-days $((F_ca_years * 365 + 1)) \
-key "$root_ca_key" -out "$dn.crt" \
-subj "/CN=ca.$domain" \
-addext "subjectAltName=DNS:$domain,DNS:*.$domain,DNS:lh,DNS:*.lh,IP:127.0.0.1,IP:::1" \
-sha256 ||
return $?
# Show CA cert details
choice "Show $dn.crt" &&
openssl x509 -noout -text -in "$dn.crt"
try_to_trust_ca "$dn.crt"
}
pem2ext() {
local pem="$1"
local val
echo ""
echo "authorityKeyIdentifier=keyid,issuer"
echo "subjectAltName = @alt_names"
val=$(_basicConstraints "$pem") && [[ -n "$val" ]] && echo "basicConstraints = $val"
val=$(_keyUsage "$pem") && [[ -n "$val" ]] && echo "keyUsage = $val"
echo ""
echo "[req]"
echo "prompt = no"
echo "req_extensions = req_ext"
echo "distinguished_name = dn"
echo "default_md = sha256"
echo "default_bits = $F_keylen"
echo ""
echo "[req_ext]"
echo "subjectAltName = @alt_names"
echo ""
echo "[dn]"
_subject "$pem"
echo ""
echo "[alt_names]"
echo ""
mapfile -t val < <(_subjectAltName "$pem" "DNS") &&
gen_alt_names DNS "${val[@]}"
mapfile -t val < <(_subjectAltName "$pem" "IP") &&
gen_alt_names IP "${val[@]}"
}
find_ca() {
local fn="$1"
if [[ "$fn" =~ ^/ ]]; then
echo "$fn"
[ -s "$fn" ]
else
for i in "${ROOT_CA_DIRS[@]}"; do
f="$i/$fn"
if [ -s "$f" ]; then
echo "$f"
return
fi
done
return 1
fi
}
try_to_trust_ca() {
local fn="$1"
local dn trust_cmd
# Trust CA cert
if dn= && [[ -d "${dn:=/etc/ca-certificates/trust-source/anchors/}" ]]; then
# Arch
trust_cmd='trust extract-compat'
elif dn= && [[ -d "${dn:=/usr/local/share/ca-certificates/}" ]]; then
# Debian & Ubuntu
trust_cmd='update-ca-certificates'
elif dn= && [[ -d "${dn:=/etc/pki/ca-trust/source/anchors/}" ]]; then
# CentOS, Fedora, RedHat
trust_cmd='update-ca-trust'
else
dn=
fi
if [[ -n "$dn" ]]; then
if choice "Trust $fn"; then
sudo cp -- "$fn" "$dn" &&
eval sudo "$trust_cmd"
else
trust_cmd=
fi
fi
if [[ -z "$trust_cmd" ]]; then
echo >&2 "Please, add \"$fn\" to your system's trusted root certificates."
return 1
fi
}
gen_ext_cnt() {
[ $# -eq 0 ] && return 1
cat <<EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
EOF
gen_alt_names DNS "$@"
}
gen_alt_names() {
local addWild
[[ "$1" = "-w" ]] && addWild=1 && shift
local prefix="$1" && shift
local idx=0
while [ $# -gt 0 ]; do
idx=$((idx + 1)) && echo "${prefix}.${idx} = $1"
[[ $addWild ]] && idx=$((idx + 1)) && echo "${prefix}.${idx} = *.$1"
shift
done
}
_subject() {
local fn=$1
local fmt=x509
[[ "${fn##*.}" = "csr" ]] && fmt=req
openssl "$fmt" -noout -subject -in "$fn" | sed 's/^subject=//' | sed 's/, /\n/g'
}
_CN() {
_subject "$1" | grep 'CN =' | awk '{print $3}'
}
# _subjectAltName <filename>.{csr|pem|crt} [filter]
# _subjectAltName cert.pem -> DNS:example.com \n IP Address:127.0.0.1
# _subjectAltName cert.pem : -> example.com \n 127.0.0.1
# _subjectAltName cert.pem DNS -> example.com
# _subjectAltName cert.pem IP -> 127.0.0.1
_subjectAltName() {
local fn=$1
local type=$2
if [[ -n "$type" ]]; then
grep -i "${type^^}" | sed -r 's/^[^:]+://'
else
cat
fi < <(
(
[[ "${fn##*.}" != "csr" ]] &&
openssl x509 -noout -ext subjectAltName -in "$fn" | tail -1 ||
openssl req -noout -text -in "$fn" | filter_next_line 'Subject Alternative Name'
) | sed -r 's/^[[:space:]]*//g; s/[[:space:]]*$//g' | sed 's/, /\n/g'
)
}
_basicConstraints() {
local pem=$1
openssl x509 -noout -ext basicConstraints -in "$pem" |
tail -1 | awk '{print $1}'
}
_keyUsage() {
local pem=$1
local i
local nr=0
while read -r i; do
[[ "$nr" -gt 0 ]] && echo -n ', '
echo -n "${i,}"
nr=$((nr + 1))
done < <(
openssl x509 -noout -ext keyUsage -in "$pem" |
tail -1 | tr ',' '\n' | awk '{print $1 $2}'
)
}
_detect_DNSes() {
local dir=$1
local name=$2
local dns cn fn
local ret=0
# Try to read alt_names from the old certificate
if fn="$dir/$name.pem" && [[ -f $fn ]]; then
dns=$(_subjectAltName "$fn" DNS)
[[ -z $dns ]] && cn=$(_CN "$fn")
fi
# Try to use the CN from the .csr as an alt_name
if fn="$dir/$name.csr" && [[ -f $fn ]]; then
if [[ -z "$dns" ]]; then
dns=$(_subjectAltName "$fn" DNS) ||
[[ -z "$cn" ]] && cn=$(_CN "$fn")
fi
fi
if [[ -z $dns ]]; then
ret=1
[[ -z $cn ]] && cn="$name" && ret=2
dns=$(echo "$cn" && echo "*.$cn")
fi
echo "$dns"
return $ret
}
norm_alt_names() {
awk -v dns=0 -v ip=0 -v alt_names=0 -v subjectAltNameVar=alt_name \
-F '=[ \t]*' \
'{
if (alt_names && $1 ~ "^DNS(\\..+)?") { dns++; print "DNS."dns" = "$2 }
else if (alt_names && $1 ~ "^IP(\\..+)?") { ip++; print "IP."ip" = "$2 }
else {
print $0;
if ($1 ~ "^[ \t]*subjectAltName") { gsub(/^@|[ \t]*$/,"",$2); subjectAltNameVar=$2 }
if ($1 == "["subjectAltNameVar"]") { alt_names=1 } else if (alt_names && $1 ~ "^\\[") { alt_names=0 }
}
}'
}
norm_ext_file() {
local fn=$1
local cnt
local norm
cnt=$(cat "$fn")
norm=$(echo "$cnt" | norm_alt_names)
if [[ "$cnt" != "$norm" ]]; then
echo "$norm" >"$fn"
fi
}
filter_next_line() {
local filter="$1"
awk -v out=0 '{
if (out) {
gsub(/^[ \t]+|[ \t]+$/, "");
print $0;
exit
}
if ($0 ~ /'"$filter"'/) { out=1 }
}'
}
choice() {
local msg=$1
local defNo=$2
local def=$([[ -z "$defNo" ]] && echo 'Y' || echo 'N')
read -r -n1 -p "$msg [$([[ -z "$defNo" ]] && echo 'Y/n' || echo 'y/N')] ?"
echo ""
# (yes da we tak) else -> NO
[[ "${REPLY:-$def}" =~ ^[yYdDwWTt]$ ]]
}
default() {
local msg=$1
local defVal=$2
read -r -p "$msg [default \"$defVal\"]: "
echo ${REPLY:-$defVal}
}
main "$@"
#!/usr/bin/env bash
_dir_=$(realpath "$(dirname "$0")")
script=casign
casign="$_dir_/$script"
main() {
local casign_vars
casign_vars=$(grep -Ev '(^#)|(^$)' "$casign" | head -20 | grep -E '^\s*\w+=')
assert_output \
"$(echo "$casign_vars" | grep '^VERSION=' | sed 's/^VERSION=//;s/\n$//')" \
"Unexpected version value" \
v
echo ""
echo "Done"
}
invoke() {
echo >&2 "$script" "$@"
if ! "$casign" "$@"; then
echo >&2 "Unexpected exit code $?"
exit 1
fi
}
assert_output() {
local expected="$1"
local message="${2:-Unexpected output}"
shift
shift
local output
output=$(invoke "$@")
if [[ "$expected" != "$output" ]]; then
echo >&2 "$message: \"$output\" != \"$expected\""
exit 2
fi
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment