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