diff options
author | Mattias Andrée <m@maandree.se> | 2025-02-27 22:46:22 +0100 |
---|---|---|
committer | Mattias Andrée <m@maandree.se> | 2025-02-27 22:46:22 +0100 |
commit | 7031307bba82993830b2391cefc96fd132b4e064 (patch) | |
tree | 8f6601da186197948ecebf066f28fdcb6bbfee61 | |
download | release-scripts-7031307bba82993830b2391cefc96fd132b4e064.tar.gz release-scripts-7031307bba82993830b2391cefc96fd132b4e064.tar.bz2 release-scripts-7031307bba82993830b2391cefc96fd132b4e064.tar.xz |
First import of scripts
Signed-off-by: Mattias Andrée <m@maandree.se>
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | LICENSE | 6 | ||||
-rw-r--r-- | README | 38 | ||||
-rwxr-xr-x | gen-checksums | 104 | ||||
-rwxr-xr-x | maandree-dl | 187 | ||||
-rwxr-xr-x | validate-checksum | 89 |
6 files changed, 426 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1189c62 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*\#* +*~ @@ -0,0 +1,6 @@ +Copyright © 2024, 2025 Mattias Andrée (m@maandree.se) + +Copying and distribution of these scripts, with or without modification, +are permitted in any medium without royalty provided the copyright +notice and this notice are preserved. These scripts is offered as-is, +without any warranty. @@ -0,0 +1,38 @@ +These scripts are offered for trust and transparency in how I secure +my software releases are not modified by an attacker. And also to enable +to you easily perform the necessarily check. These scripts are licenced +so that you can adapt them to your hosting of your own software. + +This is how it works: when I make a software release, a create and push +a git tag, I know that my local git repositry is clean. This is used +as the reference for truth. I also create a tarball for a static release. +I then use ./gen-checksums which checks the tarball, along with my +non-static releases (created by pushing the git tags), against my local +git repositry. Once all releases have been validated, ./gen-checksums +outputs the checksums for each tarball, using a number of hash functions. +The checksums are not specifically tied to the tarballs, but rather listed +as known good checksums. + +The checksums are published to my website, where all static files are +signed, so the checksum listing can be trusted. + +When creating a package for a distribution, I download the tarball for +the used mirror, and validate it against the checksum list using +./validate-checksum which prints the checksum for a selected hash +function. ./validate-checksum is primary intended for first party +packaging. + +./maandree-dl can be used by package maintainers. It will download +and validate the latest release (or a specific release of your choosing), +but it will also fail if there are important changes that could effect +how the packaging should be performed. ./maandree-dl will download the +release from an arbitrary mirror (and try others until it finds one that +is available). This is good for binary releases, but for releases that +are built by the user from source, the release file should first be +downloaded from the best mirror (./maandree-dl will validate the tarball +if it's already downloaded). + +Additionally, I sign all git commits and git tags, however these +signatures eventually become outdated as the used PGP key expires (or +is revoked). The signatures for the checksum listings are always kept +up to date with the key. diff --git a/gen-checksums b/gen-checksums new file mode 100755 index 0000000..785aff6 --- /dev/null +++ b/gen-checksums @@ -0,0 +1,104 @@ +#!/bin/sh + +# Copyright © 2024, 2025 Mattias Andrée (m@maandree.se) +# +# Copying and distribution of this script, with or without modification, +# are permitted in any medium without royalty provided the copyright +# notice and this notice are preserved. This script is offered as-is, +# without any warranty. + + +set -e + +if ! test $# = 2 || test -t 0 || test -t 1; then + printf 'usage: %s tarball git-dir < version-info-file > checksum-listing\n' "$0" >&2 + exit 1 +fi + +tarball="$1" +gitdir="$2" + +urls="$(grep '^Tarball: ' | cut -d ' ' -f 2 | grep -v '^https://maandree.se/static/')" + +workdir="$$.tmpdir" +rm -rf -- "${workdir}" + +refdir="${workdir}/reference" +mkdir -p -- "${refdir}" +gunzip < "${tarball}" | (cd "${refdir}" && tar -x) +if test "$(printf '%s\n' "${refdir}"/* | wc -l)" = 1; then + refdir="$(printf '%s\n' "${refdir}"/*)" +fi + +ln -s -- "$(cd -- "${gitdir}" && pwd)" "${workdir}/gitdir" + +add_dirs () { + while read f; do + printf '%s\n' "$f" + while :; do + d="$(printf '%s\n' "$f" | sed 's|/[^/]*$||')" + if test "$d" = "$f"; then + break + fi + printf '%s\n' "$d" + f="$d" + done + done +} + +gitfiles="$( (cd -- "${gitdir}" && git ls-files) | add_dirs | sort -u | tee -- "${workdir}/gitfiles")" +test -n "${gitfiles}" +tarfiles="$( (cd -- "${refdir}" && find) | sed 's/^\.\///' | sed '/^\.$/d' | add_dirs | sort -u | tee -- "${workdir}/tarfiles")" +test -n "${tarfiles}" +diff -u -- "${workdir}/gitfiles" "${workdir}/tarfiles" >&2 +# the following line makes the safe assumption that $workdir (and $refdir) does not contain {} +xargs -I {} diff -u "${refdir}"/{} "${workdir}/gitdir"/{} < "${workdir}/gitfiles" >&2 + +getandcheck () { + url="$1" + number="$2" + curl -sL -- "${url}" > "${workdir}/downloaded-${number}.tar.gz" + checkdir="${workdir}/downloaded" + rm -rf -- "${checkdir}" + mkdir -p -- "${checkdir}" + gunzip < "${workdir}/downloaded-${number}.tar.gz" | (cd "${checkdir}" && tar -x) + if test "$(printf '%s\n' "${checkdir}"/* | wc -l)" = 1; then + checkdir="$(printf '%s\n' "${checkdir}"/*)" + fi + diff -ur -- "${refdir}" "${checkdir}" >&2 + rm -rf -- "${checkdir}" +} + +nr=1 +for url in $urls; do + getandcheck "${url}" $(( nr++ )) +done + +genfor () { + tool="$1" + name="$2" + sum="$(${tool} < "${tarball}" | cut -d ' ' -f 1)" + sums="$(printf '%s checksum: %s\n' "${name}" "${sum}")" + number=1 + for url in $urls; do + sum="$($tool < "${workdir}/downloaded-${number}.tar.gz" | cut -d ' ' -f 1)" + test -n "$sum" + sums="$(printf '%s\n%s checksum: %s\n' "${sums}" "${name}" "${sum}")" + : $(( number++ )) + done + printf '%s\n' "${sums}" | sort -u +} + +genfor sha224sum SHA224 +genfor sha256sum SHA256 +genfor sha384sum SHA384 +genfor sha512sum SHA512 +genfor sha512-224sum SHA512/224 +genfor sha512-256sum SHA512/256 +genfor sha3-224sum SHA3-224 +genfor sha3-256sum SHA3-256 +genfor sha3-384sum SHA3-384 +genfor sha3-512sum SHA3-512 +genfor b2sum BLAKE2b + +rm -rf -- "${workdir}" diff --git a/maandree-dl b/maandree-dl new file mode 100755 index 0000000..029adde --- /dev/null +++ b/maandree-dl @@ -0,0 +1,187 @@ +#!/bin/sh +signature_key=3683C4B70CFA859F0173F2CCE0DD13EBFC7D5E3E + + +# Copyright © 2024, 2025 Mattias Andrée (m@maandree.se) +# +# Copying and distribution of this script, with or without modification, +# are permitted in any medium without royalty provided the copyright +# notice and this notice are preserved. This script is offered as-is, +# without any warranty. + + +set -e + +fetchinfo () { + printf '%s\n' "${relpage}" | \ + sed 's/<[^>]*>//g' | \ + sed 's/[[:space:]]\{1,\}/ /g' | \ + sed 's/^ //' | \ + sed 's/ $//' | \ + grep "^$1:" || : +} + +readinfo () { + _line="$(fetchinfo "$@" | cut -d : -f 2- | sed 's/^ //')" + test "$(printf '%s\n' "${_line}" | wc -l)" = 1 || return 1 + printf '%s\n' "${_line}" +} + +readmultiinfo () { + _line="$(fetchinfo "$@" | cut -d : -f 2- | sed 's/^ //')" + printf '%s\n' "${_line}" +} + +checkhash_ () { + _file="$1" + _use="$2" + _algorithm="$3" + _expect="$(readmultiinfo "${_algorithm} checksum")" + + test -n "${_expect}" && which "${_use}" >/dev/null || return 1 + + _actual="$("${_use}" -- "${_file}" | cut -d ' ' -f 1)" + test -n "${_actual}" || return 1 + for _known in ${_expect}; do + if test "${_actual}" = "${_known}"; then + echo ok + return 0 + fi + done + printf '%s checksum for %s was not recognised\n' "${_algorithm}" "${_file}" >&2 + echo bad +} + +checkhash () { + set +e + _file="$1" + printf '%s %s\n' \ + sha224sum SHA224 \ + sha256sum SHA256 \ + sha384sum SHA384 \ + sha512sum SHA512 \ + sha512-224sum SHA512/224 \ + sha512-256sum SHA512/256 \ + sha3-224sum SHA3-224 \ + sha3-256sum SHA3-256 \ + sha3-384sum SHA3-384 \ + sha3-512sum SHA3-512 \ + b2sum BLAKE2b \ + | ( + _checked=0 + _result=ok + while read tool name; do + _result="$(checkhash_ "${_file}" "${tool}" "${name}")" + if test "${_result}" = ok; then + _checked=1 + elif test "${_result}" = bad; then + _checked=1 + _result=bad + return 1 + else + : skipped + fi + done + if test "${_checked}" = 0; then + printf '%s\n' 'No supported checksum found' >&2 + return 1 + fi + echo "${_result}" + ) + ret=$? + set -e + return $ret +} + +signature_key="$(printf '%s\n' "${signature_key}" | tr -d ' ')" + +set -v + +package="$1" +version="$2" + +if test -z "$version"; then + version=latest +fi + +sigkey="$(curl -L -- "https://maandree.se/.signkey")" +if test ! "${sigkey}" = "${signature_key}"; then + printf '\n\033[1m%s\033[m,' 'Expected signature keyfile seems to be out of date' >&2 + printf ' %s' 'have a look at https://maandree.se/ to find the newest and verify that it' >&2 + printf ' %s' 'has been signed by the previous key, continue until you find and old key' >&2 + printf ' %s' 'in the signature chain that is signed by '"${signature_key}"' (or older' >&2 + printf ' %s' 'that you trust). Once verified, update `signature_key` at the top of' >&2 + printf ' %s' 'this file to be the newest key, which should be '"${sigkey}"', and' >&2 + printf ' %s' 'import it into your key collection of PGP keys.' >&2 + printf '\n' >&2 + exit 1 +fi + +relurl="https://maandree.se/rel/${package}/${version}.html" +relpage="$(curl -L -- "${relurl}")" +relpagesig="$(curl -L -- "${relurl}".sig)" + +sigtest="$(printf '%s\n' "${relpage}" | (printf '%s\n' "${relpagesig}" | gpg --status-fd=8 --verify - /dev/fd/9) 9<&0 8>&1 1>&2)" +if ! printf '%s\n' "${sigtest}" | grep -q '^\[GNUPG:\] VALIDSIG'" ${sigkey} "; then + printf '\n\033[1m%s\033[m\n' 'The release metadata page seems to be signed with an unexpected key.' >&2 + exit 1 +fi + +relversion="$(readinfo 'This version')" +test -n "${relversion}" +test "${version}" = latest || test "${relversion}" = "${version}" +version="${relversion}" + +tarurls="$(readmultiinfo 'Tarball')" +tarurls="$(echo "${tarurls}" | grep '\.tar\.gz$' || :)" +test -n "${tarurls}" +unpack='gzip -d | tar -x' +tarext='tar.gz' +tardir="${package}-${version}" +tarfile="${package}-${version}.${tarext}" + +if test -f "${tarfile}"; then + status="$(checkhash "${tarfile}")" + test -n "${status}" + test "${status}" = ok +else + downloaded=0 + for tarurl in ${tarurls}; do + if ! curl -L -- "${tarurl}" > "${tarfile}"; then + rm -f -- "${tarfile}" + continue + fi + downloaded=1 + status="$(checkhash "${tarfile}")" + test -n "${status}" + test "${status}" = ok + break + done + (( downloaded )) +fi + +(fetchinfo 'License' ; fetchinfo '.* dependencies' ; fetchinfo '.* instruction' ; fetchinfo 'News') > new-relmeta +if test -f relmeta; then + diff -u relmeta new-relmeta +fi +mv new-relmeta relmeta + +actualtardir="$(gzip -d < "${tarfile}" | tar -t | head -n 1 | cut -d / -f 1)" +quote () { + printf '%s\n' | sed "s/'/'"'\\'"''/g" | sed '1s/^/'\'/ | sed '$s/$/'\'/ +} +if test ! "${actualtardir}" = "${tardir}"; then + unpack="${unpack} && mv -- $(quote "${actualtardir}") $(quote "${tardir}")" +fi +unpack="(${unpack})" +reldata="$(printf '%s = %s\n' \ + VERSION "${version}" \ + DVERSION "$(printf '%s\n' "${version}" | tr - .)" \ + TARBALL "${tarfile}" \ + DIRECTORY "${tardir}" \ + UNPACK "${unpack}" \ + )" + +if (! test -f release-data.mk) || printf '%s\n' "${reldata}" || diff - release-data.mk >/dev/null; then + printf '%s\n' "${reldata}" > release-data.mk +fi diff --git a/validate-checksum b/validate-checksum new file mode 100755 index 0000000..6155620 --- /dev/null +++ b/validate-checksum @@ -0,0 +1,89 @@ +#!/bin/sh +signature_key=3683C4B70CFA859F0173F2CCE0DD13EBFC7D5E3E + + +# Copyright © 2025 Mattias Andrée (m@maandree.se) +# +# Copying and distribution of this script, with or without modification, +# are permitted in any medium without royalty provided the copyright +# notice and this notice are preserved. This script is offered as-is, +# without any warranty. + + +set -e + +usage () { + printf 'usage: %s hasher file\n' "$0" >&2 + exit 1 +} + +get_algo () { + if test "$1" = sha224sum; then echo SHA224 + elif test "$1" = sha256sum; then echo SHA256 + elif test "$1" = sha384sum; then echo SHA384 + elif test "$1" = sha512sum; then echo SHA512 + elif test "$1" = sha512-224sum; then echo SHA512/224 + elif test "$1" = sha512-256sum; then echo SHA512/256 + elif test "$1" = sha3-224sum; then echo SHA3-224 + elif test "$1" = sha3-256sum; then echo SHA3-256 + elif test "$1" = sha3-384sum; then echo SHA3-384 + elif test "$1" = sha3-512sum; then echo SHA3-512 + elif test "$1" = b2sum; then echo BLAKE2b + else + false + fi +} + +signature_key="$(printf '%s\n' "${signature_key}" | tr -d ' ')" + +hasher="$(printf '%s\n' "$1" | sed 's/s$//')" +file="$2" + +if ! algo="$(get_algo "${hasher}")" || test ! -f "${file}"; then + usage +fi + + +hash="$(${hasher} -- "${file}" | cut -d ' ' -f 1 | tr 'A-F' 'a-f')" + +pkgname="$(basename -- "${file}" | sed -n 's/-[^-]*\.tar\.gz$//p')" +pkgver="$(basename -- "${file}" | sed -n 's/^.*-\([^-]*\)\.tar\.gz$/\1/p')" + +if test -z "${pkgname}" || test -z "${pkgver}"; then + usage +fi + +url="https://maandree.se/rel/$pkgname/$pkgver.html" + +page="$(curl -sL "${url}")" +sigpage="$(curl -sL "${url}.sig")" + + +sigkey="$(curl -L -- "https://maandree.se/.signkey")" +if test ! "${sigkey}" = "${signature_key}"; then + printf '\n\033[1m%s\033[m,' 'Expected signature keyfile seems to be out of date' >&2 + printf ' %s' 'have a look at https://maandree.se/ to find the newest and verify that it' >&2 + printf ' %s' 'has been signed by the previous key, continue until you find and old key' >&2 + printf ' %s' 'in the signature chain that is signed by '"${signature_key}"' (or older' >&2 + printf ' %s' 'that you trust). Once verified, update `signature_key` at the top of' >&2 + printf ' %s' 'this file to be the newest key, which should be '"${sigkey}"', and' >&2 + printf ' %s' 'import it into your key collection of PGP keys.' >&2 + printf '\n' >&2 + exit 1 +fi + +sigtest="$(printf '%s\n' "${page}" | (printf '%s\n' "${sigpage}" | gpg --status-fd=8 --verify - /dev/fd/9) 9<&0 8>&1 1>&2)" +if ! printf '%s\n' "${sigtest}" | grep -q '^\[GNUPG:\] VALIDSIG'" ${sigkey} "; then + printf '\n\033[1m%s\033[m\n' 'The release metadata page seems to be signed with an unexpected key.' >&2 + exit 1 +fi + +if ! printf '%s\n' "${page}" | sed 's/<[^>]*>//g' | grep -q '^\s*'"$algo"' checksum: '"${hash}"'\s*$'; then + printf '\n\033[1m%s\033[m\n' 'Checksum not whitelisted' >&2 + exit 1 +fi + +if test -t 1; then + printf '\nChecksum OK:\n' +fi +printf '%s\n' "${hash}" |