aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMattias Andrée <m@maandree.se>2025-02-27 22:46:22 +0100
committerMattias Andrée <m@maandree.se>2025-02-27 22:46:22 +0100
commit7031307bba82993830b2391cefc96fd132b4e064 (patch)
tree8f6601da186197948ecebf066f28fdcb6bbfee61
downloadrelease-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--.gitignore2
-rw-r--r--LICENSE6
-rw-r--r--README38
-rwxr-xr-xgen-checksums104
-rwxr-xr-xmaandree-dl187
-rwxr-xr-xvalidate-checksum89
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 @@
+*\#*
+*~
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..fcdd716
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README b/README
new file mode 100644
index 0000000..2050388
--- /dev/null
+++ b/README
@@ -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}"