#!/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 "${@}"