--- /dev/null
+zeasypki -- easy PKI
+This is helper script for managing X.509 TLS PKI.
+
+ECDSA keypairs are handled with GnuTLS'es certtool.
+GOST keypairs are handled with PyGOST'es utilities
+(http://www.pygost.cypherpunks.ru).
+
+CA certificates have 10 years validity lifetime.
+EE certificates have 365 days one.
+EE certificates contain only domain name and a country.
+
+Edit zeasypki to suit your needs and working environment. Probably you
+want to change goston(), that activates PyGOST venv and key encryption
+procedures.
+
+* Create CA keypairs:
+ $ mkdir mypki && cd mypki
+ $ zeasypki ca ecdsa ecdsa-root.com
+ $ zeasypki ca gost gost-root.ru
+
+ $ zeasypki list-ca
+ ca/ecdsa/ecdsa-root.com
+ ca/gost/gost-root.ru
+
+ $ print ca/ecdsa/ecdsa-root.com/*
+ cer.pem
+ key.pem
+
+* Optionally encrypt them (that also can be done with EE keypairs too):
+ $ zeasypki encrypt ca/ecdsa/ecdsa-root.com
+ [GnuPG is invoked here]
+ $ print ca/ecdsa/ecdsa-root.com/*
+ cer.pem
+ key.pem.enc
+
+* Create EE keypairs:
+ $ zeasypki new ee/ecdsa/ecdsa-root.com/some.domain.com
+
+* Renew then EE keypairs:
+ $ zeasypki renew ee/ecdsa/ecdsa-root.com/some.domain.com
+
+* To get DANE SHA256 fingerprint:
+ $ zeasypki dane KEY
+
+* To get full PEM-encoded keypair:
+ $ zeasypki keypair KEY
+
+* To get remind (https://dianne.skoll.ca/projects/remind/) compatible
+ calendar of certificates expiration times:
+ $ zeasypki rem
--- /dev/null
+#!/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