#!/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