]> Sergey Matveev's repositories - zeasypki.git/commitdiff
Initial commit
authorSergey Matveev <stargrave@stargrave.org>
Thu, 17 Mar 2022 10:50:24 +0000 (13:50 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Thu, 17 Mar 2022 11:17:11 +0000 (14:17 +0300)
README [new file with mode: 0644]
zeasypki [new file with mode: 0755]

diff --git a/README b/README
new file mode 100644 (file)
index 0000000..344b3d5
--- /dev/null
+++ b/README
@@ -0,0 +1,50 @@
+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
diff --git a/zeasypki b/zeasypki
new file mode 100755 (executable)
index 0000000..ba52090
--- /dev/null
+++ b/zeasypki
@@ -0,0 +1,240 @@
+#!/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