diff --git a/Makefile b/Makefile index 09e943b..2cd3f04 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ chmod a+x $@ P = cassh -V = 0 +V = 2 PREFIX = /usr/local MANPREFIX = ${PREFIX}/man diff --git a/cassh-keyfile.1 b/cassh-keyfile.1 index bd1e777..570d2d2 100644 --- a/cassh-keyfile.1 +++ b/cassh-keyfile.1 @@ -11,7 +11,7 @@ .\" along with this software. If not, see .\" . .\" -.Dd April 07, 2022 +.Dd April 20, 2022 .Dt CASSH-KEYFILE 1 .Os .Sh NAME diff --git a/cassh-keyfile.sh b/cassh-keyfile.sh index 4490a09..5094b8e 100644 --- a/cassh-keyfile.sh +++ b/cassh-keyfile.sh @@ -27,7 +27,7 @@ fi cassh_command=$2 needs_agent=false case $cassh_command in -issue) +issue|revoke) needs_agent=true ;; esac diff --git a/cassh.1 b/cassh.1 index aa4400d..24529ac 100644 --- a/cassh.1 +++ b/cassh.1 @@ -11,7 +11,7 @@ .\" along with this software. If not, see .\" . .\" -.Dd March 01, 2022 +.Dd April 20, 2022 .Dt CASSH 1 .Os .Sh NAME @@ -30,13 +30,19 @@ .Bk -words .Cm mkfile .Ic authorized_keys -.Op options +.Op options ... .Ek .Nm .Bk -words .Cm mkfile .Ic known_hosts -.Op hostnames +.Op hostnames ... +.Ek +.Nm +.Bk -words +.Cm revoke +.Op Fl qv +.Ar .Ek .Sh DESCRIPTION .Nm @@ -56,9 +62,14 @@ A Certification Authority directory consists of a .Pa ./ca.pub file corresponding to the public key of it, a .Pa ./pubkeys/ -directory which holds the public keys to be signed, and an optional -.Pa ./serial.txt -file holding the current serial number for the issued certificates. +directory which holds the public keys to be signed, an optional +.Pa ./krl +file corresponding to the last issued Key Revocation List, and optional +.Pa ./ca_serial.txt +and +.Pa ./krl_serial.txt +files corresponding to the current serial number for the issued certificates +and Key Revocation Lists. .Pp The following commands are available to .Nm : @@ -84,9 +95,15 @@ The recognized tokens are: A literal .Sq % . .It \&%C -The Certification Authority private key comment. +The Certification Authority private key comment field as reported by +.Xr ssh-add 1 , +or the string +.Sq cassh +if there is no comment reported. .It %f -The basename of the public key being signed. +The basename of the public key being signed, without +.Sq .pub +suffix. .El .Pp .Ar key_id @@ -100,37 +117,56 @@ accepts the tokens %% and %f. After token expansion, all recognized options are passed down to .Xr ssh-keygen 1 process. -.It Cm mkfile Ic authorized_keys Op Ar options +.It Cm mkfile Ic authorized_keys Op Ar options ... Write an .Ic authorized_keys file on standard output corresponding to the current Certification Authority. .Ar options -is copied verbatim to the output, and +are concatenated with commas and copied verbatim to the output. .Cm cert-authority -is always added. +is always added to the options list. See .Xr sshd 8 AUTHORIZED_KEYS FILE FORMAT for details. -.It Cm mkfile Ic known_hosts Op Ar hostnames +.It Cm mkfile Ic known_hosts Op Ar hostnames ... Write a .Ic known_hosts file on standard output corresponding to the current Certification Authority. .Ar hostnames -is copied verbatim to the output. +are concatenated with commas and copied verbatim to the output. See .Xr sshd 8 SSH_KNOWN_HOSTS FILE FORMAT for details. +.It Cm revoke Oo Fl qv Oc Ar +Generates a Key Revocation List for the current Certification Authority. +All recognized options are passed down to +.Xr ssh-keygen 1 +process. +See +.Xr ssh-keygen 1 KEY REVOCATION LISTS +for details on the file format for input files. +If +.Pa ./krl +exists, +.Cm revoke +will update. +.Pa ./krl +can be synced back with the input files by first removing it. .El .Sh FILES -.Bl -tag -width MMMMMMMMMMMMMM -compact +.Bl -tag -width MMMMMMMMMMMMMMMMMM -compact .It Pa ./ca.pub Certification Authority public key .It Pa ./pubkeys/ Directory containing the public keys to be signed -.It Pa ./serial.txt -Last issued serial +.It Pa ./krl +Key Revocation List +.It Pa ./ca_serial.txt +Last issued serial for certificates +.It Pa ./krl_serial.txt +Last issued serial for KRLs .El .Sh EXIT STATUS .Ex -std diff --git a/cassh.sh b/cassh.sh index 209b2b7..c1456c8 100644 --- a/cassh.sh +++ b/cassh.sh @@ -17,8 +17,9 @@ usage() Usage: ${0##*/} issue [-hqv] [-I key_id] [-n principals] [-V validity_interval] - ${0##*/} mkfile authorized_keys [options] - ${0##*/} mkfile known_hosts [hostnames] + ${0##*/} mkfile authorized_keys [options ...] + ${0##*/} mkfile known_hosts [hostnames ...] + ${0##*/} revoke [-qv] file ... EOF exit 1 } @@ -29,29 +30,11 @@ err() exit 1 } -strip_leading_zeros() +# Returns the comment from the loaded secret key in ssh-agent, if any is +# present. +get_ca_sk_comment_from_pk() { - _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" | { + 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 @@ -59,57 +42,67 @@ get_ca_comment_from_sk() break fi done) - echo "${_comment:-${pk_fp#*:}}" + echo "${_comment:-}" } } -_template_fmt() +format() { - _allowed_chars=$1 - _char=$2 - if [ "X$_char" = X% ]; then - echo % - return $? - fi + _s=$1 + shift - case $_char in - [$_allowed_chars]) - ;; - *) - return 1 - ;; - esac - - _v=$(eval echo '${_template_fmt_'"$_char"':-}') - if [ -z "$_v" ]; then + _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 - 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 + if [ -z "${_c:-}" ]; then return 1 + elif [ X"${_c}" = X% ]; then + _out=$_out% + else + eval "_out=$_out\$_token_${_c}" || return 1 fi - _out=$_out$_t _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" } @@ -138,64 +131,42 @@ main_issue() usage fi - if [ ! -f "$PATH_CA_PUB" ]; then - err "no $PATH_CA_PUB found" - fi - if ! ssh-add $qflag $vflag -T "$PATH_CA_PUB"; then - err "can't use CA key" - fi - if [ ! -d "$PATH_PUBKEYS_DIR/" ]; then - err "no pubkeys directory found" - fi - if [ ! -f "$PATH_CA_SERIAL" ]; then - date -u +%Y%m%d000000000 >"$PATH_CA_SERIAL" + echo 1 >"$PATH_CA_SERIAL" fi read -r serial <"$PATH_CA_SERIAL" - # 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 "$PATH_CA_PUB") - find "$PATH_PUBKEYS_DIR/" -type f -name '*.pub' ! -name '*-cert.pub' | { - rc=0 + 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/} - _template_fmt_f=$pkname - id=$(template Cf "$key_id_fmt") + 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=$(template f "$principals_fmt") + principals=$(format "$principals_fmt" \ + f "$pkname") 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 "$PATH_CA_SERIAL") - - if [ $rc -ne 0 ]; then - break + if [ $? -ne 0 ]; then + exit 1 fi + + serial=$(($serial + 1)) + echo $serial >"$PATH_CA_SERIAL" done - return $rc } } @@ -219,19 +190,11 @@ main_mkfile() case $file in authorized_keys) - if [ $# -gt 1 ]; then - usage - fi - options=cert-authority${1:+,$1} - printf "%s " "$options" + printf "%s " "$(strjoin , cert-authority "$@")" ;; known_hosts) - if [ $# -gt 1 ]; then - usage - fi - hostnames=${1:-} - if [ -n "$hostnames" ]; then - printf "@cert-authority %s " "$hostnames" + if [ $# -gt 0 ]; then + printf "@cert-authority %s " "$(strjoin , "$@")" else printf "@cert-authority " fi @@ -244,10 +207,42 @@ main_mkfile() 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=./serial.txt +PATH_CA_SERIAL=./ca_serial.txt +PATH_KRL=./krl +PATH_KRL_SERIAL=./krl_serial.txt PATH_PUBKEYS_DIR=./pubkeys if [ $# -lt 1 ]; then @@ -259,5 +254,6 @@ shift case $cmd in issue) main_issue "$@" ;; mkfile) main_mkfile "$@" ;; +revoke) main_revoke "$@" ;; *) usage ;; esac