diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9b0f9006..e47a3017 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -166,6 +166,23 @@ jobs: run: sudo apt-get install -y lld - run: make validate-elf-symbols + validate-keyring: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - run: make validate-keyring + + validate-dist-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + - run: make dist-release + - name: check release artefacts + run: |- + ls -la release/*/ + cat release/*/libpathrs.sha256sum + build: runs-on: ubuntu-latest strategy: @@ -521,6 +538,8 @@ jobs: - check-lint-nohack - validate-cbindgen - validate-elf-symbols + - validate-keyring + - validate-dist-release - build - rustdoc - doctest diff --git a/.gitignore b/.gitignore index 057f876a..668c93f3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ /target **/*.rs.bk +# Releases directory. +/release + # pkg-config generated by install.sh. /pathrs.pc diff --git a/CHANGELOG.md b/CHANGELOG.md index 191ea321..ede1512a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - `install.sh` now accepts `--rust-target` and `--rust-buildmode` as parameters to make cross-compilation workflows easier to write (in particular, this is needed for runc's release scripts). +- We now produce signed release artefacts for our releases (though currently + only in the form of signed source and `cargo vendor` tarballs). The accepted + set of signing keys are available in [`libpathrs.keyring`](./libpathrs.keyring). ### Changed ### - The `O_PATH` resolver for `procfs` now has an additional bit of hardening diff --git a/MAINTAINERS b/MAINTAINERS new file mode 100644 index 00000000..1241335d --- /dev/null +++ b/MAINTAINERS @@ -0,0 +1 @@ +Aleksa Sarai (@cyphar) diff --git a/Makefile b/Makefile index cd0760cf..16629c1d 100644 --- a/Makefile +++ b/Makefile @@ -131,6 +131,10 @@ validate-cbindgen: validate-elf-symbols: release ./hack/check-elf-symbols.sh ./target/release/libpathrs.so +.PHONY: validate-keyring +validate-keyring: + ./hack/keyring-validate.sh + .PHONY: test-rust-doctest test-rust-doctest: $(CARGO_LLVM_COV) --no-report --branch --doc @@ -176,3 +180,9 @@ install: release @echo "[Sleeping for 3 seconds.]" @sleep 3s ./install.sh + +GPG_KEYID ?= cyphar@cyphar.com + +.PHONY: dist-release +dist-release: + ./hack/release.sh -S $(GPG_KEYID) diff --git a/hack/keyring-addkey.sh b/hack/keyring-addkey.sh new file mode 100755 index 00000000..ef0d1324 --- /dev/null +++ b/hack/keyring-addkey.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2023-2025 SUSE LLC. +# Copyright (C) 2023 Open Containers Authors +# Copyright (C) 2026 Aleksa Sarai +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -Eeuxo pipefail + +project="libpathrs" +root="$(readlink -f "$(dirname "${BASH_SOURCE[0]}")/..")" +keyring_file="$root/$project.keyring" + +function bail() { + echo "$@" >&2 + exit 1 +} + +[[ "$#" -eq 2 ]] || bail "usage: $0 " + +github_handle="${1}" +gpg_keyid="${2}" + +cat >>"$keyring_file" < +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -Eeuo pipefail + +project="libpathrs" +root="$(readlink -f "$(dirname "${BASH_SOURCE[0]}")/..")" + +function log() { + echo "[*]" "$@" >&2 +} + +function bail() { + log "$@" + exit 1 +} + +# Temporary GPG keyring for messing around with. +tmp_gpgdir="$(mktemp -d --tmpdir "$project-validate-tmpkeyring.XXXXXX")" +trap 'rm -r "$tmp_gpgdir"' EXIT + +function gpg_user() { + local user=$1 + shift + gpg --homedir="$tmp_gpgdir" --no-default-keyring --keyring="$user.keyring" "$@" +} + +# Get the set of MAINTAINERS. +readarray -t maintainers < <(sed -E 's|.* <.*> \(@?(.*)\)$|\1|' <"$root/MAINTAINERS") +echo "------------------------------------------------------------" +echo "$project maintainers:" +printf " * %s\n" "${maintainers[@]}" +echo "------------------------------------------------------------" + +# Create a dummy gpg keyring from the set of MAINTAINERS. +while IFS="" read -r username || [ -n "$username" ]; do + curl -sSL "https://github.com/$username.gpg" | gpg_user "$username" --import +done < <(printf '%s\n' "${maintainers[@]}") + +# Make sure all of the keys in the keyring have a github=... comment. +awk <"$root/$project.keyring" ' + /^-----BEGIN PGP PUBLIC KEY BLOCK-----$/ { key_idx++; in_pgp=1; has_comment=0; } + + # PGP comments are never broken up over several lines, and we only have one + # comment entry in our keyring file anyway. + in_pgp && /^Comment:.* github=\w+.*/ { has_comment=1 } + + /^-----END PGP PUBLIC KEY BLOCK-----$/ { + if (!has_comment) { + print "[!] Key", key_idx, "in '$project'.keyring is missing a github= comment." + exit 1 + } + } +' + +echo "------------------------------------------------------------" +echo "$project release managers:" +sed -En "s|^Comment:.* github=(\w+).*| * \1|p" <"$root/$project.keyring" | sort -u +echo "------------------------------------------------------------" +gpg --show-keys <"$root/$project.keyring" +echo "------------------------------------------------------------" + +# Check that each entry in the keyring is actually a maintainer's key. +while IFS="" read -d $'\0' -r block || [ -n "$block" ]; do + username="$(sed -En "s|^Comment:.* github=(\w+).*|\1|p" <<<"$block")" + + # FIXME: This is to work around codespell thinking that f-p-r is a + # misspelling of some other word, and the lack of support for inline + # ignores in codespell. + fprfield="f""p""r" + + # Check the username is actually a maintainer. This is just a sanity check, + # since you can put whatever you like in the Comment field. + [ -f "$tmp_gpgdir/$username.keyring" ] || bail "User $username in $project.keyring is not a maintainer!" + grep "(@$username)$" "$root/MAINTAINERS" >/dev/null || bail "User $username in $project.keyring is not a maintainer!" + + # Check that the key in the block actually matches a known key for that + # maintainer. Note that a block can contain multiple keys, so we need to + # check all of them. Since we have to handle multiple keys anyway, we'll + # also verify all of the subkeys (this is simpler to implement anyway since + # the --with-colons format outputs fingerprints for both primary and + # subkeys in the same way). + # + # Fingerprints have a field 1 of $fprfield and field 10 containing the + # fingerprint. See + # for more details. + while IFS="" read -r key || [ -n "$key" ]; do + gpg_user "$username" --list-keys --with-colons | grep "$fprfield:::::::::$key:" >/dev/null || + bail "(Sub?)Key $key in $project.keyring is NOT actually one of $username's keys!" + log "Successfully verified $username's (sub?)key $key is legitimate." + done < <(gpg --show-keys --with-colons <<<"$block" | + grep "^$fprfield:" | cut -d: -f10) +done < <(awk <"$root/$project.keyring" ' + /^-----BEGIN PGP PUBLIC KEY BLOCK-----$/ { in_block=1 } + in_block { print } + /^-----END PGP PUBLIC KEY BLOCK-----$/ { in_block=0; printf("\0"); } +') diff --git a/hack/readlinkf.sh b/hack/readlinkf.sh new file mode 100644 index 00000000..26cd42ed --- /dev/null +++ b/hack/readlinkf.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# SPDX-License-Identifier: CC0-1.0 +# readlinkf: POSIX-compliant implementation of readlink -f. +# Author: Koichi Nakashima +# Licensed under the Creative Commons Zero v1.0 Universal license. +# + +# Copied verbatim from v1.1.0 of . + +# POSIX compliant version +readlinkf_posix() { + [ "${1:-}" ] || return 1 + max_symlinks=40 + CDPATH='' # to avoid changing to an unexpected directory + + target=$1 + [ -e "${target%/}" ] || target=${1%"${1##*[!/]}"} # trim trailing slashes + [ -d "${target:-/}" ] && target="$target/" + + cd -P . 2>/dev/null || return 1 + while [ "$max_symlinks" -ge 0 ] && max_symlinks=$((max_symlinks - 1)); do + if [ ! "$target" = "${target%/*}" ]; then + case $target in + /*) cd -P "${target%/*}/" 2>/dev/null || break ;; + *) cd -P "./${target%/*}" 2>/dev/null || break ;; + esac + target=${target##*/} + fi + + if [ ! -L "$target" ]; then + target="${PWD%/}${target:+/}${target}" + printf '%s\n' "${target:-/}" + return 0 + fi + + # `ls -dl` format: "%s %u %s %s %u %s %s -> %s\n", + # , , , , + # , , , + # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/ls.html + link=$(ls -dl -- "$target" 2>/dev/null) || break + target=${link#*" $target -> "} + done + return 1 +} diff --git a/hack/release.sh b/hack/release.sh new file mode 100755 index 00000000..9fcb693b --- /dev/null +++ b/hack/release.sh @@ -0,0 +1,190 @@ +#!/bin/bash +# SPDX-License-Identifier: MPL-2.0 +# release.sh: configurable signed-artefact release script +# Copyright (C) 2016-2025 SUSE LLC +# Copyright (C) 2026 Aleksa Sarai +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +set -Eeuo pipefail +# shellcheck source=./readlinkf.sh +source "$(dirname "${BASH_SOURCE[0]}")/readlinkf.sh" + +## ---> +# Project-specific options and functions. In *theory* you shouldn't need to +# touch anything else in this script in order to use this elsewhere. +project="libpathrs" +root="$(readlinkf_posix "$(dirname "${BASH_SOURCE[0]}")/..")" + +# These functions allow you to configure how the defaults are computed. +function get_host_target() { rustc --print host-tuple ; } +function get_version() { cargo metadata --format-version=1 | jq -rM '.packages[] | select(.name == "pathrs") | .version' ; } + +# Any pre-configuration steps should be done here -- for instance ./configure. +function setup_project() { true ; } + +# This function takes an output path as an argument, where the built +# (preferably static) binary should be placed. +function build_project() { + # TODO: Figure out what we should do for builds... + true +} + +# Generates a vendor.tar.zstd file to "$1" (set to "-" to get it to stdout). +function generate_vendor() { + local vendor_tar="$1" + + local tmpvendor + tmpvendor="$(mktemp -dt "$project-vendor.XXXXXX")" + # shellcheck disable=SC2064 # We want to expand the variables immediately. + trap "rm -rf '$tmpvendor'" RETURN + + cargo vendor --versioned-dirs "$tmpvendor/vendor" + tar cv -f "$vendor_tar" -C "$tmpvendor/" vendor/ +} +# End of the easy-to-configure portion. +## <--- + +# Print usage information. +function usage() { + echo "usage: release.sh [-h] [-v ] [-c ] [-o ]" >&2 + echo " [-H ] [-S ]" >&2 +} + +# Log something to stderr. +function log() { + echo "[*]" "$@" >&2 +} + +# Log something to stderr and then exit with 0. +function quit() { + log "$@" + exit 0 +} + +# Conduct a sanity-check to make sure that GPG provided with the given +# arguments can sign something. Inability to sign things is not a fatal error. +function gpg_cansign() { + gpg "$@" --clear-sign /dev/null +} + +# When creating releases we need to build (ideally static) binaries, an archive +# of the current commit, and generate detached signatures for both. +keyid="" +version="" +commit="HEAD" +hashcmd="sha256sum" +while getopts ":c:H:h:o:S:v:" opt; do + case "$opt" in + c) + commit="$OPTARG" + ;; + H) + hashcmd="$OPTARG" + ;; + h) + usage ; exit 0 + ;; + o) + outputdir="$OPTARG" + ;; + S) + keyid="$OPTARG" + ;; + v) + version="$OPTARG" + ;; + :) + echo "Missing argument: -$OPTARG" >&2 + usage ; exit 1 + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + usage ; exit 1 + ;; + esac +done + +# Run project setup first... +( set -x ; setup_project ) + +# Generate the defaults for version and so on *after* argument parsing and +# setup_project, to avoid calling get_version() needlessly. +version="${version:-$(get_version)}" +outputdir="${outputdir:-release/$version}" + +log "[[ $project ]]" +log "version: $version" +log "commit: $commit" +log "output_dir: $outputdir" +log "key: ${keyid:-(default)}" +log "hash_cmd: $hashcmd" + +# Make explicit what we're doing. +set -x + +# Make the release directory. +rm -rf "$outputdir" && mkdir -p "$outputdir" + +# Build project. +# TODO: Figure out what we should do for builds... +#for target in "${targets[@]}"; do +# target="${target//\//.}" +# os="$(cut -d. -f1 <<<"$target")" +# arch="$(cut -d. -f2 <<<"$target")" +# GOOS="$os" GOARCH="$arch" build_project "$outputdir/$project.$target" +#done + +# Generate vendor.tar.zst. +generate_vendor - | zstd -11 >"$outputdir/$project.vendor.tar.zst" + +# Generate new archive. +git archive --format=tar --prefix="$project-$version/" "$commit" | xz > "$outputdir/$project-$version.tar.xz" + +# Generate sha256 checksums for everything. +( cd "$outputdir" ; "$hashcmd" "$project"* > "$project.$hashcmd" ; ) + +# Set up the gpgflags. +gpgflags=() +[[ -z "$keyid" ]] || gpgflags+=("--default-key=$keyid") +gpg_cansign "${gpgflags[@]}" || quit "Could not find suitable GPG key, skipping signing step." + +# Make explicit what we're doing. +set -x + +# Check that the keyid is actually in the $project.keyring by signing a piece +# of dummy text then verifying it against the list of keys in that keyring. +tmp_gpgdir="$(mktemp -d --tmpdir "$project-sign-tmpkeyring.XXXXXX")" +trap 'rm -r "$tmp_gpgdir"' EXIT + +tmp_project_gpgflags=("--homedir=$tmp_gpgdir" "--no-default-keyring" "--keyring=$project.keyring") +gpg "${tmp_project_gpgflags[@]}" --import <"$root/$project.keyring" + +gpg "${gpgflags[@]}" --clear-sign <<<"[This is test text used for $project release scripts. $(date --rfc-email)]" | + gpg "${tmp_project_gpgflags[@]}" --verify || bail "Signing key ${keyid:-DEFAULT} is not in trusted $project.keyring list!" + +# Make sure the signer is okay with the list of keys in the keyring (once this +# release is signed, distributions will trust this keyring). +cat >&2 < +sub ed25519 2022-09-30 [S] [expires: 2030-03-25] + B64E4955B29FA3D463F2A9062897FAD2B7E9446F +sub cv25519 2022-09-30 [E] [expires: 2030-03-25] + 0C23601C4F4561640663556524325218CEA61CB8 +sub ed25519 2022-09-30 [A] [expires: 2030-03-25] + A6BBD7976DBC7617FC73737D2374658C6654AF23 + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: github=cyphar + +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 +=Ab7w +-----END PGP PUBLIC KEY BLOCK----- +