#!/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 . 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 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< ... Commands: issue Issue certificate expiring in days with update URL for archive public key signed with root secret key verify Check and update certificates and verify feed index against signature EOF exit 1 } main() { case "${1:-}" in 'issue') [ ${#} -eq 6 ] || usage;; 'verify') [ ${#} -eq 3 ] || usage;; *) usage;; esac srand $((${$} + $(time))) : ${ROOT=} "${@}" } main "${@}"