cassh/cassh.sh

264 lines
4.7 KiB
Bash
Raw Normal View History

2022-04-07 15:48:50 +02:00
#!/bin/sh
# cassh - Manager for an OpenSSH Certification Authority
#
# Written in 2022 by Lucas
#
# To the extent possible under law, the author(s) have dedicated all
# copyright and related and neighboring rights to this software to the
# public domain worldwide. This software is distributed without any
# warranty.
# You should have received a copy of the CC0 Public Domain Dedication
# along with this software. If not, see
# <http://creativecommons.org/publicdomain/zero/1.0/>.
usage()
{
cat - <<EOF >&2
Usage:
${0##*/} issue [-hqv] [-I key_id] [-n principals]
[-V validity_interval]
2022-04-18 04:51:33 +02:00
${0##*/} mkfile authorized_keys [options]
${0##*/} mkfile known_hosts [hostnames]
2022-04-07 15:48:50 +02:00
EOF
exit 1
}
err()
{
printf "%s: %s\n" "${0##*/}" "$*" >&2
exit 1
}
strip_leading_zeros()
{
_s=$1
if [ -z "$_s" ]; then
return
fi
while [ X"${_s#0}" != X"$_s" ]; do
_s=${_s#0}
done
echo "${_s:-0}"
}
strcmp()
{
_r=$(expr "X$1" "$2" "X$3")
[ "${_r:-0}" -eq 1 ]
}
# Returns comment from the ssh-agent if any is returned, otherwise it
# returns the public key's fingerprint.
get_ca_comment_from_sk()
{
ssh-keygen -lf "$1" | {
read -r pk_sz pk_fp pk_extra
_comment=$(ssh-add -l | while read -r sk_sz sk_fp sk_extra; do
if [ "X$sk_fp" = "X$pk_fp" ]; then
echo "${sk_extra% (*)}"
break
fi
done)
echo "${_comment:-${pk_fp#*:}}"
}
}
_template_fmt()
{
_allowed_chars=$1
_char=$2
if [ "X$_char" = X% ]; then
echo %
return $?
fi
case $_char in
[$_allowed_chars])
;;
*)
return 1
;;
esac
_v=$(eval echo '${_template_fmt_'"$_char"':-}')
if [ -z "$_v" ]; then
return 1
fi
echo "$_v"
}
template()
{
_allowed=$1
_s=$2
_out=
while [ "${_s#*%}" != "$_s" ]; do
_t=${_s#*%}
_out=$_out${_s%"%"$_t}
_s=$_t
_c=${_s%${_s#?}}
_t=$(_template_fmt "$_allowed" "$_c")
if [ $? -ne 0 ]; then
return 1
fi
_out=$_out$_t
_s=${_s#$_c}
done
_out=$_out$_s
echo "$_out"
}
main_issue()
{
hflag=
key_id_fmt=%C/%f
nflag=false
principals_fmt=
qflag=
validity_interval=always:forever
vflag=
while getopts hI:n:qV:v flag; do
case $flag in
h) hflag=-h ;;
I) key_id_fmt=$OPTARG ;;
n) nflag=true principals_fmt=$OPTARG ;;
q) qflag=-q ;;
V) validity_interval=$OPTARG ;;
v) vflag=${vflag:--}v ;;
*) usage ;;
esac
done
shift $(($OPTIND - 1))
if [ $# -ne 0 ]; then
usage
fi
2022-04-12 01:40:43 +02:00
if [ ! -f "$PATH_CA_PUB" ]; then
err "no $PATH_CA_PUB found"
2022-04-07 15:48:50 +02:00
fi
2022-04-12 01:40:43 +02:00
if ! ssh-add $qflag $vflag -T "$PATH_CA_PUB"; then
2022-04-07 15:48:50 +02:00
err "can't use CA key"
fi
2022-04-12 01:40:43 +02:00
if [ ! -d "$PATH_PUBKEYS_DIR/" ]; then
2022-04-07 15:48:50 +02:00
err "no pubkeys directory found"
fi
2022-04-12 01:40:43 +02:00
if [ ! -f "$PATH_CA_SERIAL" ]; then
date -u +%Y%m%d000000000 >"$PATH_CA_SERIAL"
2022-04-07 15:48:50 +02:00
fi
2022-04-12 01:40:43 +02:00
read -r serial <"$PATH_CA_SERIAL"
2022-04-07 15:48:50 +02:00
# Remove NNNNNNNNN suffix
serial_date=${serial%?????????}
current_date=$(date -u +%Y%m%d)
if strcmp "$current_date" ">" "$serial_date"; then
serial_date=$current_date
serial_counter=0
else
# Remove YYYYmmdd prefix and leading
serial_counter=$(strip_leading_zeros "${serial#????????}")
fi
serial=$(printf "%s%09u\n" "$serial_date" "$serial_counter")
2022-04-12 01:40:43 +02:00
_template_fmt_C=$(get_ca_comment_from_sk "$PATH_CA_PUB")
find "$PATH_PUBKEYS_DIR/" -type f -name '*.pub' ! -name '*-cert.pub' | {
2022-04-07 15:48:50 +02:00
rc=0
while read -r pk; do
pkname=${pk%.pub}
2022-04-12 01:40:43 +02:00
pkname=${pkname#$PATH_PUBKEYS_DIR/}
2022-04-07 15:48:50 +02:00
_template_fmt_f=$pkname
id=$(template Cf "$key_id_fmt")
2022-04-12 01:40:43 +02:00
set -- -I "$id" -Us "$PATH_CA_PUB" \
$hflag $qflag $vflag \
2022-04-07 15:48:50 +02:00
-V "$validity_interval" -z "$serial"
if $nflag; then
principals=$(template f "$principals_fmt")
ssh-keygen "$@" -n "$principals" "$pk"
else
ssh-keygen "$@" "$pk"
fi || rc=1
serial_counter=$(($serial_counter + 1))
if [ $serial_counter -ge 1000000000 ]; then
err "can't issue more certificates today"
fi
serial=$(printf "%s%09u\n" "$serial_date" \
2022-04-12 01:40:43 +02:00
"$serial_counter" | tee "$PATH_CA_SERIAL")
2022-04-07 15:48:50 +02:00
if [ $rc -ne 0 ]; then
break
fi
done
return $rc
}
}
main_mkfile()
{
while getopts : flag; do
case $flag in
*) usage ;;
esac
done
shift $(($OPTIND - 1))
if [ $# -lt 1 ]; then
usage
fi
file=$1
shift
2022-04-12 01:40:43 +02:00
if [ ! -f "$PATH_CA_PUB" ]; then
err "no $PATH_CA_PUB found"
2022-04-07 15:48:50 +02:00
fi
case $file in
authorized_keys)
if [ $# -gt 1 ]; then
usage
fi
options=cert-authority${1:+,$1}
printf "%s " "$options"
;;
known_hosts)
if [ $# -gt 1 ]; then
usage
fi
hostnames=${1:-}
if [ -n "$hostnames" ]; then
printf "@cert-authority %s " "$hostnames"
else
printf "@cert-authority "
fi
;;
*)
err "unknown file \"$file\""
;;
esac
2022-04-12 01:40:43 +02:00
cat "$PATH_CA_PUB"
2022-04-07 15:48:50 +02:00
}
set -u
2022-04-12 01:40:43 +02:00
PATH_CA_PUB=./ca.pub
PATH_CA_SERIAL=./serial.txt
PATH_PUBKEYS_DIR=./pubkeys
2022-04-07 15:48:50 +02:00
if [ $# -lt 1 ]; then
usage
fi
cmd=$1
shift
case $cmd in
issue) main_issue "$@" ;;
mkfile) main_mkfile "$@" ;;
*) usage ;;
esac