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