#!/bin/sh -e
# 
# Mandos key generator - create new keys for a Mandos client
# 
# Copyright © 2008-2024 Teddy Hogeborn
# Copyright © 2008-2024 Björn Påhlsson
# 
# This file is part of Mandos.
#
# Mandos is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
#     Mandos is distributed in the hope that it will be useful, but
#     WITHOUT ANY WARRANTY; without even the implied warranty of
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#     GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with Mandos.  If not, see <http://www.gnu.org/licenses/>.
# 
# Contact the authors at <mandos@recompile.se>.
# 

VERSION="1.8.19"

KEYDIR="/etc/keys/mandos"
KEYTYPE=RSA
KEYLENGTH=4096
SUBKEYTYPE=RSA
SUBKEYLENGTH=4096
KEYNAME="`hostname --fqdn 2>/dev/null || hostname`"
KEYEMAIL=""
KEYCOMMENT=""
KEYEXPIRE=0
TLS_KEYTYPE=ed25519
FORCE=no
SSH=yes
KEYCOMMENT_ORIG="$KEYCOMMENT"
mode=keygen

if [ ! -d "$KEYDIR" ]; then
    KEYDIR="/etc/mandos/keys"
fi

# Parse options
TEMP=`getopt --options vhpF:d:t:l:s:L:n:e:c:x:T:fS \
    --longoptions version,help,password,passfile:,dir:,type:,length:,subtype:,sublength:,name:,email:,comment:,expire:,tls-keytype:,force,no-ssh \
    --name "$0" -- "$@"`

help(){
basename="`basename "$0"`"
cat <<EOF
Usage: $basename [ -v | --version ]
       $basename [ -h | --help ]
   Key creation:
       $basename [ OPTIONS ]
   Encrypted password creation:
       $basename { -p | --password } [ --name NAME ] [ --dir DIR]
       $basename { -F | --passfile } FILE [ --name NAME ] [ --dir DIR]

Key creation options:
  -v, --version         Show program's version number and exit
  -h, --help            Show this help message and exit
  -d DIR, --dir DIR     Target directory for key files
  -t TYPE, --type TYPE  OpenPGP key type.  Default is RSA.
  -l BITS, --length BITS
                        OpenPGP key length in bits.  Default is 4096.
  -s TYPE, --subtype TYPE
                        OpenPGP subkey type.  Default is RSA.
  -L BITS, --sublength BITS
                        OpenPGP subkey length in bits.  Default 4096.
  -n NAME, --name NAME  Name of key.  Default is the FQDN.
  -e ADDRESS, --email ADDRESS
                        Email address of OpenPGP key.  Default empty.
  -c TEXT, --comment TEXT
                        Comment field for OpenPGP key.  Default empty.
  -x TIME, --expire TIME
                        OpenPGP key expire time.  Default is none.
                        See gpg(1) for syntax.
  -T TYPE, --tls-keytype TYPE
                        TLS key type.  Default is ed25519.
  -f, --force           Force overwriting old key files.

Password creation options:
  -p, --password        Create an encrypted password using the key in
                        the key directory.  All options other than
                        --dir and --name are ignored.
  -F FILE, --passfile FILE
                        Encrypt a password from FILE using the key in
                        the key directory.  All options other than
                        --dir and --name are ignored.
  -S, --no-ssh          Don't get SSH key or set "checker" option.
EOF
}

eval set -- "$TEMP"
while :; do
    case "$1" in
	-p|--password) mode=password; shift;;
	-F|--passfile) mode=password; PASSFILE="$2"; shift 2;;
	-d|--dir) KEYDIR="$2"; shift 2;;
	-t|--type) KEYTYPE="$2"; shift 2;;
	-s|--subtype) SUBKEYTYPE="$2"; shift 2;;
	-l|--length) KEYLENGTH="$2"; shift 2;;
	-L|--sublength) SUBKEYLENGTH="$2"; shift 2;;
	-n|--name) KEYNAME="$2"; shift 2;;
	-e|--email) KEYEMAIL="$2"; shift 2;;
	-c|--comment) KEYCOMMENT="$2"; shift 2;;
	-x|--expire) KEYEXPIRE="$2"; shift 2;;
	-T|--tls-keytype) TLS_KEYTYPE="$2"; shift 2;;
	-f|--force) FORCE=yes; shift;;
	-S|--no-ssh) SSH=no; shift;;
	-v|--version) echo "$0 $VERSION"; exit;;
	-h|--help) help; exit;;
	--) shift; break;;
	*) echo "Internal error" >&2; exit 1;;
    esac
done
if [ "$#" -gt 0 ]; then
    echo "Unknown arguments: '$*'" >&2
    exit 1
fi

SECKEYFILE="$KEYDIR/seckey.txt"
PUBKEYFILE="$KEYDIR/pubkey.txt"
TLS_PRIVKEYFILE="$KEYDIR/tls-privkey.pem"
TLS_PUBKEYFILE="$KEYDIR/tls-pubkey.pem"

# Check for some invalid values
if [ ! -d "$KEYDIR" ]; then
    echo "$KEYDIR not a directory" >&2
    exit 1
fi
if [ ! -r "$KEYDIR" ]; then
    echo "Directory $KEYDIR not readable" >&2
    exit 1
fi

if [ "$mode" = keygen ]; then
    if [ ! -w "$KEYDIR" ]; then
	echo "Directory $KEYDIR not writeable" >&2
	exit 1
    fi
    if [ -z "$KEYTYPE" ]; then
	echo "Empty key type" >&2
	exit 1
    fi

    if [ -z "$KEYNAME" ]; then
	echo "Empty key name" >&2
	exit 1
    fi

    if [ -z "$KEYLENGTH" ] || [ "$KEYLENGTH" -lt 512 ]; then
	echo "Invalid key length" >&2
	exit 1
    fi

    if [ -z "$KEYEXPIRE" ]; then
	echo "Empty key expiration" >&2
	exit 1
    fi

    # Make FORCE be 0 or 1
    case "$FORCE" in
	[Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]) FORCE=1;;
	[Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|*) FORCE=0;;
    esac

    if { [ -e "$SECKEYFILE" ] || [ -e "$PUBKEYFILE" ] \
	     || [ -e "$TLS_PRIVKEYFILE" ] \
	     || [ -e "$TLS_PUBKEYFILE" ]; } \
	&& [ "$FORCE" -eq 0 ]; then
	echo "Refusing to overwrite old key files; use --force" >&2
	exit 1
    fi

    # Set lines for GnuPG batch file
    if [ -n "$KEYCOMMENT" ]; then
	KEYCOMMENTLINE="Name-Comment: $KEYCOMMENT"
    fi
    if [ -n "$KEYEMAIL" ]; then
	KEYEMAILLINE="Name-Email: $KEYEMAIL"
    fi

    # Create temporary gpg batch file
    BATCHFILE="`mktemp -t mandos-keygen-batch.XXXXXXXXXX`"
    TLS_PRIVKEYTMP="`mktemp -t mandos-keygen-privkey.XXXXXXXXXX`"
fi

if [ "$mode" = password ]; then
    # Create temporary encrypted password file
    SECFILE="`mktemp -t mandos-keygen-secfile.XXXXXXXXXX`"
fi

# Create temporary key ring directory
RINGDIR="`mktemp -d -t mandos-keygen-keyrings.XXXXXXXXXX`"

# Remove temporary files on exit
trap "
set +e; \
test -n \"$SECFILE\" && shred --remove \"$SECFILE\"; \
test -n \"$TLS_PRIVKEYTMP\" && shred --remove \"$TLS_PRIVKEYTMP\"; \
shred --remove \"$RINGDIR\"/sec* 2>/dev/null;
test -n \"$BATCHFILE\" && rm --force \"$BATCHFILE\"; \
rm --recursive --force \"$RINGDIR\";
tty --quiet && stty echo; \
" EXIT

set -e

umask 077

if [ "$mode" = keygen ]; then
    # Create batch file for GnuPG
    cat >"$BATCHFILE" <<-EOF
	Key-Type: $KEYTYPE
	Key-Length: $KEYLENGTH
	Key-Usage: sign,auth
	Subkey-Type: $SUBKEYTYPE
	Subkey-Length: $SUBKEYLENGTH
	Subkey-Usage: encrypt
	Name-Real: $KEYNAME
	$KEYCOMMENTLINE
	$KEYEMAILLINE
	Expire-Date: $KEYEXPIRE
	#Preferences: <string>
	#Handle: <no-spaces>
	#%pubring pubring.gpg
	#%secring secring.gpg
	%no-protection
	%commit
	EOF

    if tty --quiet; then
	cat <<-EOF
	Note: Due to entropy requirements, key generation could take
	anything from a few minutes to SEVERAL HOURS.  Please be
	patient and/or supply the system with more entropy if needed.
	EOF
	echo -n "Started: "
	date
    fi

    # Generate TLS private key
    if certtool --generate-privkey --password='' \
		--outfile "$TLS_PRIVKEYTMP" --sec-param ultra \
		--key-type="$TLS_KEYTYPE" --pkcs8 --no-text 2>/dev/null; then
	
	# Backup any old key files
	if cp --backup=numbered --force "$TLS_PRIVKEYFILE" "$TLS_PRIVKEYFILE" \
	      2>/dev/null; then
	    shred --remove "$TLS_PRIVKEYFILE" 2>/dev/null || :
	fi
	if cp --backup=numbered --force "$TLS_PUBKEYFILE" "$TLS_PUBKEYFILE" \
	      2>/dev/null; then
	    rm --force "$TLS_PUBKEYFILE"
	fi
	cp --archive "$TLS_PRIVKEYTMP" "$TLS_PRIVKEYFILE"
	shred --remove "$TLS_PRIVKEYTMP" 2>/dev/null || :

	## TLS public key

	# First try certtool from GnuTLS
	if ! certtool --password='' --load-privkey="$TLS_PRIVKEYFILE" \
	     --outfile="$TLS_PUBKEYFILE" --pubkey-info --no-text \
	     2>/dev/null; then
	    # Otherwise try OpenSSL
	    if ! openssl pkey -in "$TLS_PRIVKEYFILE" \
		 -out "$TLS_PUBKEYFILE" -pubout; then
		rm --force "$TLS_PUBKEYFILE"
		# None of the commands succeded; give up
		return 1
	    fi
	fi
    fi

    # Make sure trustdb.gpg exists;
    # this is a workaround for Debian bug #737128
    gpg --quiet --batch --no-tty --no-options --enable-dsa2 \
	--homedir "$RINGDIR" \
	--import-ownertrust < /dev/null
    # Generate a new key in the key rings
    gpg --quiet --batch --no-tty --no-options --enable-dsa2 \
	--homedir "$RINGDIR" --trust-model always \
	--gen-key "$BATCHFILE"
    rm --force "$BATCHFILE"

    if tty --quiet; then
	echo -n "Finished: "
	date
    fi

    # Backup any old key files
    if cp --backup=numbered --force "$SECKEYFILE" "$SECKEYFILE" \
	2>/dev/null; then
	shred --remove "$SECKEYFILE" 2>/dev/null || :
    fi
    if cp --backup=numbered --force "$PUBKEYFILE" "$PUBKEYFILE" \
	2>/dev/null; then
	rm --force "$PUBKEYFILE"
    fi

    FILECOMMENT="Mandos client key for $KEYNAME"
    if [ "$KEYCOMMENT" != "$KEYCOMMENT_ORIG" ]; then
	FILECOMMENT="$FILECOMMENT ($KEYCOMMENT)"
    fi

    if [ -n "$KEYEMAIL" ]; then
	FILECOMMENT="$FILECOMMENT <$KEYEMAIL>"
    fi

    # Export key from key rings to key files
    gpg --quiet --batch --no-tty --no-options --enable-dsa2 \
	--homedir "$RINGDIR" --armor --export-options export-minimal \
	--comment "$FILECOMMENT" --output "$SECKEYFILE" \
	--export-secret-keys
    gpg --quiet --batch --no-tty --no-options --enable-dsa2 \
	--homedir "$RINGDIR" --armor --export-options export-minimal \
	--comment "$FILECOMMENT" --output "$PUBKEYFILE" --export
fi

if [ "$mode" = password ]; then

    # Make SSH be 0 or 1
    case "$SSH" in
	[Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]) SSH=1;;
	[Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|*) SSH=0;;
    esac

    if [ $SSH -eq 1 ]; then
	# The -q option is new in OpenSSH 9.8
	for ssh_keyscan_quiet in "-q " ""; do
	    for ssh_keytype in ecdsa-sha2-nistp256 ed25519 rsa; do
		set +e
		ssh_fingerprint="`ssh-keyscan ${ssh_keyscan_quiet}-t $ssh_keytype localhost 2>/dev/null`"
		err=$?
		set -e
		if [ $err -ne 0 ]; then
		    ssh_fingerprint=""
		    continue
		fi
		if [ -n "$ssh_fingerprint" ]; then
		    ssh_fingerprint="${ssh_fingerprint#localhost }"
		    break 2
		fi
	    done
	done
    fi

    # Import key into temporary key rings
    gpg --quiet --batch --no-tty --no-options --enable-dsa2 \
	--homedir "$RINGDIR" --trust-model always --armor \
	--import "$SECKEYFILE"
    gpg --quiet --batch --no-tty --no-options --enable-dsa2 \
	--homedir "$RINGDIR" --trust-model always --armor \
	--import "$PUBKEYFILE"

    # Get fingerprint of key
    FINGERPRINT="`gpg --quiet --batch --no-tty --no-options \
	--enable-dsa2 --homedir "$RINGDIR" --trust-model always \
	--fingerprint --with-colons \
	| sed --quiet \
	--expression='/^fpr:/{s/^fpr:.*:\\([0-9A-Z]*\\):\$/\\1/p;q}'`"

    test -n "$FINGERPRINT"

    if [ -r "$TLS_PUBKEYFILE" ]; then
       KEY_ID="$(certtool --key-id --hash=sha256 \
    		       --infile="$TLS_PUBKEYFILE" 2>/dev/null || :)"

       if [ -z "$KEY_ID" ]; then
	   KEY_ID=$(openssl pkey -pubin -in "$TLS_PUBKEYFILE" \
			    -outform der \
			| openssl sha256 \
			| sed --expression='s/^.*[^[:xdigit:]]//')
       fi
       test -n "$KEY_ID"
    fi

    FILECOMMENT="Encrypted password for a Mandos client"

    while [ ! -s "$SECFILE" ]; do
	if [ -n "$PASSFILE" ]; then
	    cat -- "$PASSFILE"
	else
	    tty --quiet && stty -echo
	    echo -n "Enter passphrase: " >/dev/tty
	    read -r first
	    tty --quiet && echo >&2
	    echo -n "Repeat passphrase: " >/dev/tty
	    read -r second
	    if tty --quiet; then
		echo >&2
		stty echo
	    fi
	    if [ "$first" != "$second" ]; then
		echo "Passphrase mismatch" >&2
		touch "$RINGDIR"/mismatch
	    else
		printf "%s" "$first"
	    fi
	fi | gpg --quiet --batch --no-tty --no-options --enable-dsa2 \
	    --homedir "$RINGDIR" --trust-model always --armor \
	    --encrypt --sign --recipient "$FINGERPRINT" --comment \
	    "$FILECOMMENT" > "$SECFILE"
	if [ -e "$RINGDIR"/mismatch ]; then
	    rm --force "$RINGDIR"/mismatch
	    if tty --quiet; then
		> "$SECFILE"
	    else
		exit 1
	    fi
	fi
    done

    cat <<-EOF
	[$KEYNAME]
	host = $KEYNAME
	EOF
    if [ -n "$KEY_ID" ]; then
	echo "key_id = $KEY_ID"
    fi
    cat <<-EOF
	fingerprint = $FINGERPRINT
	secret =
	EOF
    sed --quiet --expression='
	/^-----BEGIN PGP MESSAGE-----$/,/^-----END PGP MESSAGE-----$/{
	    /^$/,${
		# Remove 24-bit Radix-64 checksum
		s/=....$//
		# Indent four spaces
		/^[^-]/s/^/    /p
	    }
	}' < "$SECFILE"
    if [ -n "$ssh_fingerprint" ]; then
	if [ -n "$ssh_keyscan_quiet" ]; then
	    echo "# Note: if the Mandos server has OpenSSH older than 9.8, the ${ssh_keyscan_quiet}"
	    echo "# option *must* be removed from the 'checker' setting below"
	fi
	echo 'checker = ssh-keyscan '"$ssh_keyscan_quiet"'-t '"$ssh_keytype"' %%(host)s 2>/dev/null | grep --fixed-strings --line-regexp --quiet --regexp=%%(host)s" %(ssh_fingerprint)s"'
	echo "ssh_fingerprint = ${ssh_fingerprint}"
    fi
fi

trap - EXIT

set +e
# Remove the password file, if any
if [ -n "$SECFILE" ]; then
    shred --remove "$SECFILE" 2>/dev/null
fi
# Remove the key rings
shred --remove "$RINGDIR"/sec* 2>/dev/null
rm --recursive --force "$RINGDIR"
