+#!/usr/bin/env zsh
+# zeasypki -- easy PKI
+# Copyright (C) 2022 Sergey Matveev <stargrave@stargrave.org>
+
+set -e
+
+CERTTOOL=${CERTTOOL:-certtool}
+GPG=${GPG:-gpg}
+KEY_ENCRYPT_RECIPIENT=${KEY_ENCRYPT_RECIPIENT:-CF60E89A59231E76E2636422AE1A8109E49857EF}
+COUNTRY=${COUNTRY:-RU}
+
+goston() {
+ path=(~/local/stow/py310/bin ~/work/pygost/pygost/asn1schemas $path)
+ export -TU PYTHONPATH pythonpath
+ pythonpath=(~/work/pygost ~/work/pyderasn)
+}
+
+key_encrypt() {
+ ${=GPG} --encrypt --recipient $KEY_ENCRYPT_RECIPIENT
+}
+
+key_decrypt() {
+ ${=GPG} --decrypt
+}
+
+# ------------------------ >8 ------------------------
+
+usage() {
+ >&2 <<EOF
+Usage:
+ \$ $0:t ca [ecdsa|gost] NAME -- new CA keypair
+ \$ $0:t list-ca -- list CA keypairs
+ \$ $0:t list -- list EE ones
+ \$ $0:t rem -- list certificate expirations
+ \$ $0:t new KEY -- new EE
+ \$ $0:t renew KEY -- renew EE
+ \$ $0:t dane KEY -- show DANE SHA256 hash
+ \$ $0:t encrypt KEY -- encrypt private key
+ \$ $0:t keypair KEY -- PEM-encoded full keypair
+EOF
+ exit 1
+}
+
+zmodload -F zsh/files b:zf_mkdir
+zmodload zsh/mapfile
+
+key_get() {
+ [[ -s $1/key.pem ]] &&
+ REPLY=`< ${1}/key.pem` ||
+ REPLY=`key_decrypt < ${1}/key.pem.enc`
+}
+
+certtool_genkey() {
+ local bits=$1
+ ${=CERTTOOL} --generate-privkey --ecc --bits $bits --no-text
+}
+
+ca_new_ecdsa() {
+ local domain=$1
+ local key=`mktemp`
+ local tmpl=`mktemp`
+ local cert=`mktemp`
+ trap "rm -f $key $tmpl $cert" HUP PIPE INT QUIT TERM EXIT
+ > $tmpl <<EOF
+dn = "cn=$domain,c=$COUNTRY"
+serial = 1
+expiration_days = 3650
+ca
+cert_signing_key
+EOF
+ certtool_genkey 512 > $key
+ ${=CERTTOOL} \
+ --generate-self-signed \
+ --load-privkey $key \
+ --template $tmpl \
+ --outfile $cert
+ reply=(${mapfile[$key]} ${mapfile[$cert]})
+}
+
+ee_key_new_ecdsa() {
+ certtool_genkey 256
+}
+
+ee_key_new_gost() {
+ goston
+ cert-selfsigned-example.py --cn does-not-matter --ai 256A --only-key
+}
+
+ee_renew_ecdsa() {
+ local ca=$1
+ local domain=$2
+ local cakey=`mktemp`
+ local key=`mktemp`
+ local tmpl=`mktemp`
+ local cert=`mktemp`
+ trap "rm -f $cakey $key $tmpl $cert" HUP PIPE INT QUIT TERM EXIT
+ key_get ca/ecdsa/$ca
+ mapfile[$cakey]=$REPLY
+ key_get ee/ecdsa/$ca/$domain
+ mapfile[$key]=$REPLY
+ > $tmpl <<EOF
+dn = "cn=$domain,c=RU"
+expiration_days = 365
+signing_key
+dns_name = "$domain"
+EOF
+ ${=CERTTOOL} \
+ --load-ca-certificate ca/ecdsa/$ca/cer.pem \
+ --load-ca-privkey $cakey \
+ --generate-certificate \
+ --load-privkey $key \
+ --template $tmpl
+}
+
+ee_renew_gost() {
+ local ca=$1
+ local domain=$2
+ goston
+ local cakey=`mktemp`
+ local key=`mktemp`
+ local cert=`mktemp`
+ trap "rm -f $cakey $key $cert" HUP PIPE INT QUIT TERM EXIT
+ key_get ca/gost/$ca
+ mapfile[$cakey]=$REPLY
+ >> $cakey < ca/gost/$ca/cer.pem
+ key_get ee/gost/$ca/$domain
+ mapfile[$key]=$REPLY
+ cert-selfsigned-example.py \
+ --issue-with $cakey \
+ --reuse-key $key \
+ --cn $domain --country $COUNTRY --ai 256A
+}
+
+ca_new_gost() {
+ local domain=$1
+ goston
+ local key=`mktemp`
+ local cert=`mktemp`
+ trap "rm -f $key $cert" HUP PIPE INT QUIT TERM EXIT
+ cert-selfsigned-example.py \
+ --ca \
+ --cn $domain \
+ --country $COUNTRY \
+ --serial 1 \
+ --ai 512C \
+ --out-key $key \
+ --out-cert $cert
+ reply=(${mapfile[$key]} ${mapfile[$cert]})
+}
+
+dane_ecdsa() {
+ ${=CERTTOOL} --key-id --hash=sha256
+}
+
+dane_gost() {
+ goston
+ cert-dane-hash.py
+}
+
+case $1 in
+(ca)
+ [[ $# -eq 3 ]] || usage
+ local algo=$2
+ local domain=$3
+ local dst=ca/$algo/$domain
+ zf_mkdir -p $dst
+ [[ -s $dst/key.pem ]] && {
+ print $dst/key.pem already exists >&2
+ exit 1
+ }
+ ca_new_${algo} $domain
+ local _umask=`umask`
+ umask 077
+ mapfile[${dst}/key.pem]=${reply[1]}
+ umask $_umask
+ mapfile[${dst}/cer.pem]=${reply[2]}
+ print $dst
+ ;;
+(encrypt)
+ [[ $# -eq 2 ]] || usage
+ local key=$2/key.pem
+ [[ -s $key ]] || {
+ print no $key found >&2
+ exit 1
+ }
+ umask 077
+ key_encrypt < $key > $key.enc
+ rm $key
+ ;;
+(new)
+ [[ $# -eq 2 ]] || usage
+ local cols=(${(s:/:)2})
+ local algo=${cols[2]}
+ local ca=${cols[3]}
+ local domain=${cols[4]}
+ local dst=ee/$algo/$ca/$domain
+ [[ $dst = $2 ]]
+ zf_mkdir -p $dst
+ [[ -s $dst/key.pem ]] && {
+ print $dst/key.pem already exists >&2
+ exit 1
+ }
+ local _umask=`umask`
+ umask 077
+ ee_key_new_${algo} > $dst/key.pem
+ umask $_umask
+ ee_renew_${algo} $ca $domain > $dst/cer.pem
+ ;;
+(renew)
+ [[ $# -eq 2 ]] || usage
+ local cols=(${(s:/:)2})
+ local algo=${cols[2]}
+ local ca=${cols[3]}
+ local domain=${cols[4]}
+ ee_renew_${algo} $ca $domain > ee/$algo/$ca/$domain/cer.pem
+ ;;
+(dane)
+ [[ $# -eq 2 ]] || usage
+ dane_${${(s:/:)2}[2]} < $2/cer.pem
+ ;;
+(keypair)
+ [[ $# -eq 2 ]] || usage
+ key_get $2
+ print -- "$REPLY"
+ cat $2/cer.pem
+ ;;
+(rem)
+ setopt GLOB_STAR_SHORT
+ export LC_ALL=C
+ for cer (**/cer.pem) {
+ date_bad_format=`certtool -i < $cer |
+ perl -ne '/Not After: \w+ (\w+ \d+ \d+:\d+):\d+ UTC (\d+)/ && print "$1 $2"'`
+ date_good_format=`date -j -f "%b %d %H:%M %Y" "$date_bad_format" +"%Y-%m-%d"`
+ print REM $date_good_format +30 MSG $cer
+ }
+ ;;
+(list) print -C1 ee/*/*/*(/on) ;;
+(list-ca) print -C1 ca/*/*(/on) ;;
+(*) usage ;;
+esac