summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xopkg-cert329
1 files changed, 329 insertions, 0 deletions
diff --git a/opkg-cert b/opkg-cert
new file mode 100755
index 0000000..82238aa
--- /dev/null
+++ b/opkg-cert
@@ -0,0 +1,329 @@
+#!/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 "${@}"