diff options
Diffstat (limited to 'opkg-cert')
-rwxr-xr-x | opkg-cert | 329 |
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 "${@}" |