#!/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
# .
usage()
{
cat - <&2
Usage:
${0##*/} issue [-hqv] [-I key_id] [-n principals]
[-V validity_interval]
${0##*/} mkfile authorized_keys [options ...]
${0##*/} mkfile known_hosts [hostnames ...]
${0##*/} revoke [-qv] file ...
EOF
exit 1
}
err()
{
printf "%s: %s\n" "${0##*/}" "$*" >&2
exit 1
}
# Returns the comment from the loaded secret key in ssh-agent, if any is
# present.
get_ca_sk_comment_from_pk()
{
ssh-keygen -lf "$1" 2>/dev/null | {
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:-}"
}
}
format()
{
_s=$1
shift
_cleanup=unset
while [ $# -ge 2 ]; do
_k=$1 _v=$2
shift 2
case $_k in
[A-Za-z])
;;
*)
return 1
;;
esac
eval "_token_${_k}=\$_v"
_cleanup=$_cleanup" _token_${_k}"
done
if [ $# -ne 0 ]; then
return 1
fi
_out=
while [ "${_s#*%}" != "$_s" ]; do
_t=${_s#*%}
_out=$_out${_s%"%"$_t}
_s=$_t
_c=${_s%${_s#?}}
if [ -z "${_c:-}" ]; then
return 1
elif [ X"${_c}" = X% ]; then
_out=$_out%
else
eval "_out=$_out\$_token_${_c}" || return 1
fi
_s=${_s#$_c}
done
_out=$_out$_s
eval "$_cleanup"
echo "$_out"
}
strjoin()
{
_c=$1
shift
_out=
for _s; do
_out=${_out:+$_out$_c}$_s
done
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 "$PATH_CA_SERIAL" ]; then
echo 1 >"$PATH_CA_SERIAL"
fi
read -r serial <"$PATH_CA_SERIAL"
if [ ! -d "$PATH_PUBKEYS_DIR" ]; then
exit 0
fi
find "$PATH_PUBKEYS_DIR" -type f -name '*.pub' ! -name '*-cert.pub' |
sort | {
ca_comment=$(get_ca_sk_comment_from_pk "$PATH_CA_PUB")
: ${ca_comment:=cassh}
while read -r pk; do
pkname=${pk%.pub}
pkname=${pkname#$PATH_PUBKEYS_DIR/}
id=$(format "$key_id_fmt" C "$ca_comment" f "$pkname")
set -- -I "$id" -Us "$PATH_CA_PUB" \
$hflag $qflag $vflag \
-V "$validity_interval" -z "$serial"
if $nflag; then
principals=$(format "$principals_fmt" \
f "$pkname")
ssh-keygen "$@" -n "$principals" "$pk"
else
ssh-keygen "$@" "$pk"
fi
if [ $? -ne 0 ]; then
exit 1
fi
serial=$(($serial + 1))
echo $serial >"$PATH_CA_SERIAL"
done
}
}
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 "$PATH_CA_PUB" ]; then
err "no $PATH_CA_PUB found"
fi
case $file in
authorized_keys)
printf "%s " "$(strjoin , cert-authority "$@")"
;;
known_hosts)
if [ $# -gt 0 ]; then
printf "@cert-authority %s " "$(strjoin , "$@")"
else
printf "@cert-authority "
fi
;;
*)
err "unknown file \"$file\""
;;
esac
cat "$PATH_CA_PUB"
}
main_revoke()
{
qflag=
vflag=
while getopts fqv flag; do
case $flag in
q) qflag=-q ;;
v) vflag=${vflag:--}v ;;
*) usage ;;
esac
done
shift $(($OPTIND - 1))
if [ ! -f "$PATH_KRL_SERIAL" ]; then
echo 1 >"$PATH_KRL_SERIAL"
fi
read -r serial <"$PATH_KRL_SERIAL"
uflag=
if [ -f "$PATH_KRL" ]; then
uflag=-u
fi
ssh-keygen -kf "$PATH_KRL" -Us "$PATH_CA_PUB" -z "$serial" \
$qflag $vflag $uflag "$@" || exit 1
serial=$(($serial + 1))
echo $serial >"$PATH_KRL_SERIAL"
}
set -u
PATH_CA_PUB=./ca.pub
PATH_CA_SERIAL=./ca_serial.txt
PATH_KRL=./krl
PATH_KRL_SERIAL=./krl_serial.txt
PATH_PUBKEYS_DIR=./pubkeys
if [ $# -lt 1 ]; then
usage
fi
cmd=$1
shift
case $cmd in
issue) main_issue "$@" ;;
mkfile) main_mkfile "$@" ;;
revoke) main_revoke "$@" ;;
*) usage ;;
esac