From a5400e398c7c8f77d09b3e5966789d38f48a47a6 Mon Sep 17 00:00:00 2001 From: Dominic Ricottone Date: Sat, 15 Oct 2022 15:51:33 -0500 Subject: [PATCH] Starting point for cryptographic utilities Much of this came from my deep dive into digital signatures for my chat application. More details in a recent blog post. These tools are not yet mature enough to warrant an installation recipe. The sign, verify, and verify-filter utilities are especially so. --- crypto/keytype | 151 ++++++++++++++++++++++++++++++++ crypto/mkhtpasswd | 39 +++++++++ crypto/mkkey | 204 +++++++++++++++++++++++++++++++++++++++++++ crypto/pgpls | 25 ++++++ crypto/pgprm | 37 ++++++++ crypto/sign | 5 ++ crypto/verify | 5 ++ crypto/verify-filter | 115 ++++++++++++++++++++++++ 8 files changed, 581 insertions(+) create mode 100755 crypto/keytype create mode 100755 crypto/mkhtpasswd create mode 100755 crypto/mkkey create mode 100755 crypto/pgpls create mode 100755 crypto/pgprm create mode 100644 crypto/sign create mode 100644 crypto/verify create mode 100644 crypto/verify-filter diff --git a/crypto/keytype b/crypto/keytype new file mode 100755 index 0000000..a66f9f3 --- /dev/null +++ b/crypto/keytype @@ -0,0 +1,151 @@ +#!/bin/bash + +name="keytype" +version="1.0" +read -r -d '' help_message <<-EOF + Inspect a cryptographic key for type and length + Usage: keytype [OPTIONS] KEYFILE + Options: + -h, --help print this message and exit + -q, --quiet suppress error messages and prompts + -v, --verbose show additional messages + -V, --version print version number and exit +EOF + +source /usr/local/lib/mylib.bash + +quiet=0 +verbose=0 +while [[ $# -gt 0 ]]; do + case "$1" in + + -h|--help) + help_msg + shift + ;; + + -q|--quiet) + debug_msg "Setting quiet option to 1 (was ${quiet})" + quiet=1 + shift + ;; + + -v|--verbose) + debug_msg "Setting verbose option to 1 (was ${verbose})" + verbose=1 + shift + ;; + + -V|--version) + version_msg + ;; + + *) + debug_msg "Argument '${1}' added to positional array" + positional+=("$1") + shift + ;; + esac +done + +try_openssh() { + keylength= + keyfingerprint= + keytype= + keycomment= + + keyinfo="$(/usr/bin/ssh-keygen -l -f "$keyfile" 2>/dev/null)" + + if [ $? -ne 0 ]; then + debug_msg "file '${keyfile}' is not an OpenSSH key" + return 1 + else + keylength="$(/usr/bin/cut --delimiter=' ' --field=1 <<<"$keyinfo")" + keyfingerprint="$(/usr/bin/cut --delimiter=' ' --field=2 <<<"$keyinfo")" + keycomment="$(/usr/bin/awk '{$1=$2=$NF="";print substr($0,3,length($0)-3)}' <<<"$keyinfo")" + keytype="$(/usr/bin/awk '{print substr($NF,2,length($NF)-2)}' <<<"$keyinfo")" + + /usr/bin/printf "%s: %s-bit %s OpenSSH key\n" "${keyfile}" "${keylength}" "${keytype}" + return 0 + fi +} + +try_openpgp() { + keyinfo= + keyprivacy= + keylength= + keytypenum= + keytype= + keyfingerprint= + + keyinfodump="$(/usr/bin/gpg2 --show-keys --with-colons "$keyfile" 2>/dev/null)" + + if [ $? -ne 0 ]; then + debug_msg "file '${keyfile}' is not an OpenPGP key" + return 1 + else + if /usr/bin/grep -e "^pub:" <<<"$keyinfodump" >/dev/null 2>&1; then + keyprivacy="public" + keyinfo="$(/usr/bin/grep -e "^pub:" <<<"$keyinfodump")" + elif /usr/bin/grep -e "^sec:" <<<"$keyinfodump" >/dev/null 2>&1; then + keyprivacy="private" + keyinfo="$(/usr/bin/grep -e "^sec:" <<<"$keyinfodump")" + else + debug_msg "file '${keyfile}' is an OpenPGP key, but it cannot be interpretted" + fi + + keylength="$(/usr/bin/cut --delimiter=':' --field=3 <<<"$keyinfo")" + keytypenum="$(/usr/bin/cut --delimiter=':' --field=4 <<<"$keyinfo")" + + case "$keytypenum" in + 1) + keytype="RSA" + ;; + + 16) + keytype="Elgamel" + ;; + + 17) + keytype="DSA" + ;; + + *) + error_msg "Unknown algorythm on OpenPGP key" + ;; + + #2 is deprecated as RSA Encrypt Only + #3 is deprecated as RSA Sign Only + #18 is reserved for Elliptic Curve but not part of the OpenPGP spec yet + #19 is reserved for ECDSA but not part of the OpenPGP spec yet + #20 is reserved; deprecated as Elgamel Encrypt or Sign + #21 is reserved for Diffie-Hellman but not part of the OpenPGP spec yet + #100-110 are reserved for experimental use + #256+ are reserved for Libgcrypt to allocate + esac + + keyfingerprint="$(/usr/bin/cut --delimiter=':' --field=5 <<<"$keyinfo")" + + /usr/bin/printf "%s: %s-bit %s %s OpenPGP key\n" "${keyfile}" "${keylength}" "${keyprivacy}" "${keytype}" + return 0 + fi +} + +try_openssl() { + #TODO: implement OpenSSL checks + : +} + +code=0 +for keyfile in "${positional[@]}"; do + if ! try_openssh; then + if ! try_openpgp; then + if ! try_openssl; then + code=1 + fi + fi + fi +done + +exit "$code" + diff --git a/crypto/mkhtpasswd b/crypto/mkhtpasswd new file mode 100755 index 0000000..25ed705 --- /dev/null +++ b/crypto/mkhtpasswd @@ -0,0 +1,39 @@ +#!/bin/bash + +name="mkhtpasswd" +version="1.0" +read -r -d '' help_message <<-EOF + Create an Apache htpasswd file + Usage: mkhtpasswd [OPTIONS] >>.htpasswd + Options: + -h, --help print this message and exit + -q, --quiet suppress error messages and prompts + -v, --verbose show additional messages + -V, --version print version number and exit +EOF + +source /usr/local/lib/mylib.bash +. /usr/local/lib/myparse.bash + +# Get username +user= +/usr/bin/printf "Enter username: " >&2 +read user +if [[ -z "$user" ]]; then + error_msg "expected non-empty username" +fi + +# Get password +passwd= +/usr/bin/printf "Enter password:" >&2 +read -s passwd +/usr/bin/printf "\n" >&2 +if [[ -z "$passwd" ]]; then + debug_msg "Using empty password" +else + debug_msg "Using a non-empty password" +fi + +# Print out the htpasswd file +/usr/bin/printf '%s:%s\n' "$user" "$(/usr/bin/openssl passwd -crypt "$passwd")" + diff --git a/crypto/mkkey b/crypto/mkkey new file mode 100755 index 0000000..49fcbcf --- /dev/null +++ b/crypto/mkkey @@ -0,0 +1,204 @@ +#!/bin/bash + +name="mkkey" +version="1.0" +read -r -d '' help_message <<-EOF + Create a cryptographic keypair + Usage: mkkey [OPTIONS] [FILE] + Options: + -a ALGO, algorithm for keypair [rsa, ed25519] + --algorithm ALGO + -h, --help print this message and exit + -q, --quiet suppress error messages and prompts + --private NAME private key file name + --public NAME public key file name + -t TYPE, type of keypair [ssh, pgp, ssl] + --type TYPE + -v, --verbose show additional messages + -V, --version print version number and exit +EOF + +source /usr/local/lib/mylib.bash + +positional=() +keytype= +keyalgo= +privkey= +pubkey= +quiet= +verbose= +while [[ $# -gt 0 ]]; do + case "$1" in + + -a|--algorithm) + debug_msg "Setting algorithm to '$2' (was ${keyalgo})" + keyalgo="$2" + shift; shift + ;; + + -h|--help) + help_msg + shift + ;; + + -q|--quiet) + debug_msg "Setting quiet option to 1 (was ${quiet})" + quiet=1 + shift + ;; + + --private) + debug_msg "Setting private key file name to '$2' (was ${privkey})" + privkey="$2" + shift; shift + ;; + + --public) + debug_msg "Setting public key file name to '$2' (was ${pubkey})" + pubkey="$2" + shift; shift + ;; + + -t|--type) + debug_msg "Setting type to '$2' (was ${keytype})" + keytype="$2" + shift; shift + ;; + + -v|--verbose) + debug_msg "Setting verbose option to 1 (was ${verbose})" + verbose=1 + shift + ;; + + -V|--version) + version_msg + ;; + + *) + debug_msg "Argument '${1}' added to positional array" + positional+=("$1") + shift + ;; + esac +done + +# handle output files +# NOTE: individual key types have additional requirements for output files +if [[ ${#positional[@]} -gt 0 ]] && [[ -z "$privkey" ]]; then + debug_msg "Moving a positional argument into private key file name option" + privkey="${positional[0]}" + positional=("${positional[@]:1}") +fi +if [[ "$privkey" = "$pubkey" ]]; then + error_msg "expected different file names for public and private key" +fi + +try_openssh() { + # if pubkey filename is specified, check if it complies with ssh-keygen + if [[ ! -z "$pubkey" ]] && [[ "$pubkey" != "${privkey}.pub" ]]; then + if [[ -z "$privkey" ]]; then + privkey="$(/usr/bin/basename "$pubkey" ".pub")" + if [[ "$privkey" = "$pubkey" ]]; then + error_msg "public key must be written to a file named like the private key" + fi + else + error_msg "public key must be written to a file named like the private key" + fi + fi + + # if privkey filename is not specified, use defaults + if [[ -z "$privkey" ]]; then + if [[ "$keyalgo" = "rsa" ]]; then + privkey="${HOME}/.ssh/id_rsa" + debug_msg "Default file for private RSA key is '${privkey}'" + pubkey="${privkey}.pub" + debug_msg "Default file for public RSA key is '${pubkey}'" + elif [[ "$keyalgo" = "ed25519" ]]; then + privkey="${HOME}/.ssh/id_ed25519" + debug_msg "Default file for private Ed25519 key is '${privkey}'" + pubkey="${privkey}.pub" + debug_msg "Default file for public Ed25519 key is '${pubkey}'" + else + error_msg "key algorithm '${keyalgo}' not implemented" + fi + fi + + # check if keypair files already exist + if [[ -f "$privkey" ]]; then + error_msg "file '${privkey}' already exists" + elif [[ -f "${pubkey}" ]]; then + error_msg "file '${pubkey}' already exists" + fi + + # make keypair + if [[ "$keyalgo" = "rsa" ]]; then + if ! /usr/bin/ssh-keygen -t rsa -b 4096 -f "$privkey"; then + error_msg "failed to generate RSA keypair" + fi + elif [[ "$keyalgo" = "ed25519" ]]; then + if ! /usr/bin/ssh-keygen -t ed25519 -f "$privkey"; then + error_msg "failed to generate Ed25519 keypair" + fi + else + error_msg "key algorithm '${keyalgo}' not implemented" + fi +} + +try_openpgp() { + : +} + +try_openssl() { + # if privkey filename is not specified, try basename of pubkey + if [[ -z "$privkey" ]]; then + if [[ -z "$pubkey" ]]; then + error_msg "expected private key file name" + else + privkey="$(/usr/bin/basename "$pubkey" ".pub")" + if [[ "$privkey" = "$pubkey" ]]; then + error_msg "expected private key file name" + fi + fi + fi + + # if pubkey filename is not specified, use privkey as basename + if [[ -z "$pubkey" ]] && [[ ! -z "$privkey" ]]; then + pubkey="${privkey}.pub" + fi + + # check if keypair files already exist + if [[ -f "$privkey" ]]; then + error_msg "file '${privkey}' already exists" + elif [[ -f "${pubkey}" ]]; then + error_msg "file '${pubkey}' already exists" + fi + + # make keypair + if [[ "$keyalgo" = "rsa" ]]; then + if ! /usr/bin/openssl genrsa -out "$privkey" 4096; then + error_msg "failed to generate RSA keypair" + elif ! /usr/bin/openssl rsa -in "$privkey" -pubout -out "$pubkey"; then + error_msg "failed to generate RSA public key" + fi + elif [[ "$keyalgo" = "ed25519" ]]; then + if ! /usr/bin/openssl genpkey -algorithm Ed25519 -out "$privkey"; then + error_msg "failed to generate Ed25519 keypair" + elif ! /usr/bin/openssl pkey -in "$privkey" -pubout -out "$pubkey"; then + error_msg "failed to generate Ed25519 public key" + fi + else + error_msg "key algorithm '${keyalgo}' not implemented" + fi +} + +if [[ "$keytype" = "ssh" ]]; then + try_openssh +elif [[ "$keytype" = "pgp" ]]; then + try_openpgp +elif [[ "$keytype" = "ssl" ]]; then + try_openssl +else + error_msg "key type '${keytype}' not implemented" +fi + diff --git a/crypto/pgpls b/crypto/pgpls new file mode 100755 index 0000000..4d983c0 --- /dev/null +++ b/crypto/pgpls @@ -0,0 +1,25 @@ +#!/bin/sh + +name="pgpls" +version="1.0" +usage_message="Usage: pgpls [OPTIONS] [KEY ..]" +help_message=$(/usr/bin/cat <<-EOF + List OpenPGP keys on the keychain + Usage: pgpls [OPTIONS] + Options: + -h, --help print this message and exit + -q, --quiet suppress error messages + -v, --version print version number and exit +EOF +) + +. /usr/local/lib/myminiparse.sh + +code=0 +if ! /usr/bin/gpg2 –list-keys; then + code=1 +fi + +# exit with stored code +exit "$code" + diff --git a/crypto/pgprm b/crypto/pgprm new file mode 100755 index 0000000..937b3c1 --- /dev/null +++ b/crypto/pgprm @@ -0,0 +1,37 @@ +#!/bin/sh + +name="pgprm" +version="1.0" +usage_message="Usage: pgprm [OPTIONS] [KEY ..]" +help_message=$(/usr/bin/cat <<-EOF + Delete an OpenPGP key from the keychain + Usage: pgprm [OPTIONS] [KEY ..] + Options: + -h, --help print this message and exit + -q, --quiet suppress error messages + -v, --version print version number and exit +EOF +) + +. /usr/local/lib/myminiparse.sh + +# loop through arguments +code=0 +for arg; do + case "$arg" in + -q|--quiet) + #ignore these + ;; + + *) + # main routine + if ! /usr/bin/gpg2 -delete-key "$arg"; then + code=1 + fi + ;; + esac +done + +# exit with stored code +exit "$code" + diff --git a/crypto/sign b/crypto/sign new file mode 100644 index 0000000..c0a381f --- /dev/null +++ b/crypto/sign @@ -0,0 +1,5 @@ +# gpg2 --detach-sign example +# if ! gpg2 --verify example.sig example >/dev/null 2>&1; then code=1; fi + +# gpg2 --clearsign example +# if ! gpg2 --verify example.asc >/dev/null 2>&1; then code=1; fi diff --git a/crypto/verify b/crypto/verify new file mode 100644 index 0000000..c0a381f --- /dev/null +++ b/crypto/verify @@ -0,0 +1,5 @@ +# gpg2 --detach-sign example +# if ! gpg2 --verify example.sig example >/dev/null 2>&1; then code=1; fi + +# gpg2 --clearsign example +# if ! gpg2 --verify example.asc >/dev/null 2>&1; then code=1; fi diff --git a/crypto/verify-filter b/crypto/verify-filter new file mode 100644 index 0000000..52f42d5 --- /dev/null +++ b/crypto/verify-filter @@ -0,0 +1,115 @@ +#!/bin/bash + +name="verify-filter" +version="1.0" +read -r -d '' help_message <<-EOF + Verify a PGP-signed file and print it + Usage: pgpcat [OPTIONS] FILE + Options: + -e, --exit exit without printing FILE if verification fails + -f FILE, --file FILE file to verify and print + -h, --help print this message and exit + -q, --quiet suppress error messages + -s SIG, --signature SIG detached signature file; if unspecified, + FILE should be armored clear-signed message + -v, --verbose show additional messages + -V, --version print version number and exit +EOF + +source /usr/local/lib/mylib.bash + +positional=() +fail_immediately=0 +msgfile= +quiet=0 +sigfile= +verbose=0 + +while [[ $# -gt 0 ]]; do + case $1 in + + -e|--exit) + debug_msg "Setting exit option to 1 (was ${fail_immediately})" + fail_immediately=1 + shift + ;; + + -f|--file) + debug_msg "Setting file option to ${2} (was ${msgfile})" + msgfile="$2" + shift; shift + ;; + + -h|--help) + help_msg + shift + ;; + + -q|--quiet) + debug_msg "Setting quiet option to 1 (was ${quiet})" + quiet=1 + shift + ;; + + -s|--signature) + debug_msg "Setting signature option to ${2} (was ${sigfile})" + sigfile="$2" + shift; shift + ;; + + -v|--verbose) + debug_msg "Setting verbose option to 1 (was ${verbose})" + verbose=1 + shift + ;; + + -V|--version) + version_msg + ;; + + *) + debug_msg "Argument '${1}' added to positional array" + positional+=("$1") + shift + ;; + esac +done + +# handle input files +if [[ -z "$msgfile" ]]; then + if [[ ${#positional[@]} -lt 1 ]]; then + error_msg "Expected input file (given none)" + else + debug_msg "Moving a positional argument into file option (${#positional[0]})" + msgfile="${positional[0]}" + positional=("${positional[@]:1}") + fi +fi + +code=0 +if [[ -z "$sigfile" ]]; then + debug_msg "'${msgfile}' is armored clearsigned" + if ! /usr/bin/local/verify --file "$msgfile"; then + code=1 + nonfatal_error_msg "Can not verify file '${msgfile}'" + + if [[ "$fail_immediately" -eq 1 ]]; then + exit "$code" + fi + fi + /usr/bin/cat "$msgfile" +else + debug_msg "'${msgfile}' is signed, '${sigfile}' is detached signature" + if ! /usr/bin/local/verify --file "$msgfile" --signature "${sigfile}"; then + code=1 + nonfatal_error_msg "Can not verify file '${msgfile}'" + + if [[ "$fail_immediately" -eq 1 ]]; then + exit "$code" + fi + fi + /usr/bin/cat "$msgfile" +fi + +exit "$code" + -- 2.45.2