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