cassh/cassh.sh

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