258 lines
4.4 KiB
Bash
258 lines
4.4 KiB
Bash
|
#!/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]
|
||
|
${0##*/} mkfile [-n principals] authorized_keys | known_hosts
|
||
|
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
|
||
|
|
||
|
if [ ! -f ca.pub ]; then
|
||
|
err "no ca.pub found"
|
||
|
fi
|
||
|
if ! ssh-add $qflag $vflag -T ca.pub; then
|
||
|
err "can't use CA key"
|
||
|
fi
|
||
|
if [ ! -d pubkeys/ ]; then
|
||
|
err "no pubkeys directory found"
|
||
|
fi
|
||
|
|
||
|
if [ ! -f serial.txt ]; then
|
||
|
date -u +%Y%m%d000000000 >serial.txt
|
||
|
fi
|
||
|
read -r serial <serial.txt
|
||
|
# 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")
|
||
|
|
||
|
_template_fmt_C=$(get_ca_comment_from_sk ca.pub)
|
||
|
find pubkeys/ -type f -name '*.pub' ! -name '*-cert.pub' | {
|
||
|
rc=0
|
||
|
while read -r pk; do
|
||
|
pkname=${pk%.pub}
|
||
|
pkname=${pkname#pubkeys/}
|
||
|
_template_fmt_f=$pkname
|
||
|
|
||
|
id=$(template Cf "$key_id_fmt")
|
||
|
set -- -I "$id" -Us ca.pub $hflag $qflag $vflag \
|
||
|
-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" \
|
||
|
"$serial_counter" | tee serial.txt)
|
||
|
|
||
|
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
|
||
|
|
||
|
if [ ! -f ca.pub ]; then
|
||
|
err "no ca.pub found"
|
||
|
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
|
||
|
|
||
|
cat ca.pub
|
||
|
}
|
||
|
|
||
|
set -u
|
||
|
|
||
|
if [ $# -lt 1 ]; then
|
||
|
usage
|
||
|
fi
|
||
|
cmd=$1
|
||
|
shift
|
||
|
|
||
|
case $cmd in
|
||
|
issue) main_issue "$@" ;;
|
||
|
mkfile) main_mkfile "$@" ;;
|
||
|
*) usage ;;
|
||
|
esac
|