#!/bin/sh
#
# opkg-cert - Issue and verify archive key certificates signed by root keys
#
# Copyright (C) 2019  Patrick McDermott
#
# This program 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.
#
# This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.

set -eu

LF='
'
CHECK_DELAY=$((60 * 60 * 24))

rand_x=
temp=
recv_cert_file=
new_fprint=
new_key=

die()
{
	local fmt="${1}"
	shift 1

	printf "%s: Error: ${fmt}\n" "${0##*/}" "${@}" 1>&2
	kill -s ABRT 0
}

info()
{
	local fmt="${1}"
	shift 1

	printf "%s: ${fmt}\n" "${0##*/}" "${@}" 1>&2
	return 0
}

time()
{
	# Based on code from <https://www.etalabs.net/sh_tricks.html> by Rich
	# Felker, with whitespace added for readability.
	printf '%d' $(($(TZ=UTC0 date "+
		(
			  (%Y - 1600) * 365
			+ (%Y - 1600) / 4
			- (%Y - 1600) / 100
			+ (%Y - 1600) / 400
			+ 1%j - 1000
			- 135140
		) * 86400
		+ (1%H - 100) * 3600
		+ (1%M - 100) * 60
		+ (1%S - 100)")))
}

srand()
{
	local x="${1}"

	case "${x}" in '' | *[!0-9]*)
		die 'Bad RNG seed'
	esac

	rand_x=${x}
}

rand()
{
	if [ x"${rand_x:-}" = x'' ]; then
		die 'RNG not seeded'
	fi
	# Increment, multiplier, and modulus values are those used in glibc.
	rand_x=$((1103515245 * ${rand_x} + 12345))
	rand_x=$((${rand_x} % 4294967296))
}

mktemp()
{
	rand
	temp="${TMPDIR:-/tmp}/tmp.${rand_x}"
	(umask 0177 && 1>"${temp}") || die 'Failed to create temporary file'
}

issue()
{
	local url="${1}"
	local days="${2}"
	local key="${3}"
	local root="${4}"
	local cert="${5}"
	shift 5
	local now=
	local root_fprint=
	local payload=

	now=$(time)
	if ! root_fprint="$("${USIGN:-usign}" -F -s "${root}")"; then
		info 'Invalid root key'
		return 1
	fi

	mktemp
	payload="${temp}"
	exec 3>"${payload}"
	printf 'V: %d\n' ${now} 1>&3
	printf 'E: %d\n' $((${now} + ${days} * 24 * 60 * 60)) 1>&3
	IFS="${LF}"
	printf 'K: %s\n' $(cat -- "${key}") 1>&3
	unset IFS
	printf 'R: %s\n' "${root_fprint}" 1>&3
	exec 3>&-
	sig="$("${USIGN:-usign}" -S -m "${payload}" -s "${root}" -x -)"

	if [ x"${cert}" = x'-' ]; then
		exec 3>&1
	else
		exec 3>"${cert}"
	fi
	printf '%s\n---\n%s\n---\n%s\n' "${url}" "$(cat "${payload}")" \
		"${sig}" 1>&3
	exec 3>&-

	rm -f -- "${payload}"

	return 0
}

recv_cert()
{
	local url="${1}"
	shift 1

	mktemp
	recv_cert_file="${temp}"
	if "${WGET:-wget}" -q -O "${recv_cert_file}" -- "${url}"; then
		return 0
	else
		return 1
	fi
}

check_cert()
{
	local cert="${1}"
	local new="${2}"
	shift 2
	local url=''
	local line=''
	local k=''
	local v=''
	local valid=0
	local expires=0
	local key=''
	local root=''
	local payload=''
	local sig=''
	local fprintf=''
	local now=

	exec 3<"${cert}"

	# URL header part
	while IFS='' read line 0<&3; do
		if [ x"${line}" = x'---' ]; then
			break
		fi
		url="${line}"
	done

	# Certificate payload part
	while IFS='' read line 0<&3; do
		IFS=': ' read k v 0<<-EOF
			${line}
			EOF
		case "${k}" in
			'V') valid="${v}";;
			'E') expires="${v}";;
			'K') key="${key}${v}${LF}";;
			'R') root="${v}";;
			'---') break;;
		esac
		payload="${payload}${line}${LF}"
	done

	# Payload signature part
	while IFS='' read line 0<&3; do
		sig="${sig}${line}${LF}"
	done

	exec 3<&-

	if ! fprint="$(printf '%s' "${key}" | "${USIGN:-usign}" -F -p -)"; then
		info 'Invalid key'
		return 1
	fi

	# Check for updates.
	if ! ${new} && recv_cert "${url}"; then
		if check_cert "${recv_cert_file}" true; then
			# Valid update received.  Remove old certificate and
			# install new certificate and key.
			info 'Received valid certificate for key %s' \
				"${new_fprint}"
			rm -f -- "${cert}"
			mv -- "${recv_cert_file}" \
				"${ROOT}/etc/opkg/keys/${new_fprint}.cert"
			printf '%s' "${new_key}" \
				1>"${ROOT}/etc/opkg/keys/${new_fprint}"
			return 0
		else
			info 'Invalid certificate from <%s>!' "${url}"
			rm -f -- "${recv_cert_file}"
		fi
	fi

	# Check dates.
	now=$(time)
	if [ "${valid}" -eq 0 ] || [ ${now} -lt "${valid}" ]; then
		if ${new}; then
			rm -f -- "${cert}"
		else
			# The date was checked previously, so this indicates the
			# clock is wrong.
			info 'Clock incorrect'
		fi
		return 1
	fi
	if [ "${expires}" -eq 0 ] || [ ${now} -gt "${expires}" ]; then
		if ! ${new}; then
			info 'Certificate for key %s expired; removing' \
				"${fprint}"
		fi
		rm -f -- "${cert}"
		return 1
	fi

	# Check signature.
	if ${new}; then
		mktemp
		printf '%s' "${payload}" 1>"${temp}"
		payload="${temp}"
		mktemp
		printf '%s' "${sig}" 1>"${temp}"
		sig="${temp}"
		if ! "${USIGN:-usign}" -q -V -p \
				"${ROOT}/etc/opkg/keys/${root}.root" \
				-m "${payload}" -x "${sig}"; then
			rm -f -- "${payload}" "${sig}"
			return 1
		fi
		rm -f -- "${payload}" "${sig}"
		new_fprint="${fprint}"
		new_key="${key}"
	fi

	return 0
}

verify()
{
	local sig="${1}"
	local msg="${2}"
	shift 2
	local last=
	local cert=

	if ! last=$(cat "${ROOT}/var/cache/opkg/last-cert-check" 2>/dev/null)
	then
		last=0
	fi
	if [ $(time) -gt $((${last} + ${CHECK_DELAY})) ]; then
		for cert in "${ROOT}/etc/opkg/keys/"*.cert; do
			check_cert "${cert}" false || :
		done
		printf '%d\n' $(time) 1>"${ROOT}/var/cache/opkg/last-cert-check"
	fi

	if { "${GUNZIP:-gunzip}" -c -- "${msg}" || cat "${msg}"; } \
			2>/dev/null | "${USIGN:-usign}" -V -q -m - \
			-P "${ROOT}/etc/opkg/keys/" -x "${sig}"; then
		return 0
	else
		return 1
	fi
}

usage()
{
	cat 0<<EOF
Usage: ${@} <command> <argument>...
Commands:
  issue <url> <days> <key> <root> <cert>  Issue certificate <cert> expiring in
                                          <days> days with <url> update URL for
                                          archive public key <key> signed with
                                          root secret key <root>
  verify <sig> <list>                     Check and update certificates and
                                          verify feed index <list> against
                                          signature <sig>
EOF
	exit 1
}

main()
{
	case "${1:-}" in
		'issue')  [ ${#} -eq 6 ] || usage;;
		'verify') [ ${#} -eq 3 ] || usage;;
		*) usage;;
	esac

	srand $((${$} + $(time)))
	: ${ROOT=}

	"${@}"
}

main "${@}"