# Copyright 2020-2024 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 # @ECLASS: verify-sig.eclass # @MAINTAINER: # Michał Górny # @SUPPORTED_EAPIS: 7 8 # @BLURB: Eclass to verify upstream signatures on distfiles # @DESCRIPTION: # verify-sig eclass provides a streamlined approach to verifying # upstream signatures on distfiles. Its primary purpose is to permit # developers to easily verify signatures while bumping packages. # The eclass removes the risk of developer forgetting to perform # the verification, or performing it incorrectly, e.g. due to additional # keys in the local keyring. It also permits users to verify # the developer's work. # # To use the eclass, start by packaging the upstream's key # as sec-keys/openpgp-keys-*. Then inherit the eclass, add detached # signatures to SRC_URI and set VERIFY_SIG_OPENPGP_KEY_PATH. The eclass # provides verify-sig USE flag to toggle the verification. # # If you need to use signify, you may want to copy distfiles into WORKDIR to # work around "Too many levels of symbolic links" error. # # A more complete guide can be found at: # https://mgorny.pl/articles/verify-sig-by-example.html # # @EXAMPLE: # Example use: # # @CODE # inherit verify-sig # # SRC_URI="https://example.org/${P}.tar.gz # verify-sig? ( https://example.org/${P}.tar.gz.sig )" # BDEPEND=" # verify-sig? ( sec-keys/openpgp-keys-example )" # # VERIFY_SIG_OPENPGP_KEY_PATH=/usr/share/openpgp-keys/example.asc # @CODE case ${EAPI} in 7|8) ;; *) die "${ECLASS}: EAPI ${EAPI:-0} not supported" ;; esac if [[ -z ${_VERIFY_SIG_ECLASS} ]]; then _VERIFY_SIG_ECLASS=1 IUSE="verify-sig" # @ECLASS_VARIABLE: VERIFY_SIG_METHOD # @PRE_INHERIT # @DESCRIPTION: # Signature verification method to use. The allowed value are: # # - minisig -- verify signatures with (base64) Ed25519 public key using app-crypt/minisign # - openpgp -- verify PGP signatures using app-crypt/gnupg (the default) # - signify -- verify signatures with Ed25519 public key using app-crypt/signify : "${VERIFY_SIG_METHOD:=openpgp}" case ${VERIFY_SIG_METHOD} in minisig) BDEPEND="verify-sig? ( app-crypt/minisign )" ;; openpgp) BDEPEND=" verify-sig? ( app-crypt/gnupg >=app-portage/gemato-20 ) " ;; signify) BDEPEND="verify-sig? ( app-crypt/signify )" ;; *) die "${ECLASS}: unknown method '${VERIFY_SIG_METHOD}'" ;; esac # @ECLASS_VARIABLE: VERIFY_SIG_OPENPGP_KEY_PATH # @DEFAULT_UNSET # @DESCRIPTION: # Path to key bundle used to perform the verification. This is required # when using default src_unpack. Alternatively, the key path can be # passed directly to the verification functions. # # The value of BROOT will be prepended to this path automatically. # # NB: this variable is also used for non-OpenPGP signatures. The name # contains "OPENPGP" for historical reasons. # @ECLASS_VARIABLE: VERIFY_SIG_OPENPGP_KEYSERVER # @DEFAULT_UNSET # @DESCRIPTION: # Keyserver used to refresh keys. If not specified, the keyserver # preference from the key will be respected. If no preference # is specified by the key, the GnuPG default will be used. # # Supported for OpenPGP only. # @ECLASS_VARIABLE: VERIFY_SIG_OPENPGP_KEY_REFRESH # @USER_VARIABLE # @DESCRIPTION: # Attempt to refresh keys via WKD/keyserver. Set it to "yes" # in make.conf to enable. Note that this requires working Internet # connection. # # Supported for OpenPGP only. : "${VERIFY_SIG_OPENPGP_KEY_REFRESH:=no}" # @FUNCTION: verify-sig_verify_detached # @USAGE: [] # @DESCRIPTION: # Read the detached signature from and verify against # it. can either be passed directly, or it defaults # to VERIFY_SIG_OPENPGP_KEY_PATH. The function dies if verification # fails. verify-sig_verify_detached() { local file=${1} local sig=${2} local key=${3} if [[ -z ${key} ]]; then if [[ -z ${VERIFY_SIG_OPENPGP_KEY_PATH} ]]; then die "${FUNCNAME}: no key passed and VERIFY_SIG_OPENPGP_KEY_PATH unset" else key="${BROOT}${VERIFY_SIG_OPENPGP_KEY_PATH}" fi fi local extra_args=() [[ ${VERIFY_SIG_OPENPGP_KEY_REFRESH} == yes ]] || extra_args+=( -R ) if [[ -n ${VERIFY_SIG_OPENPGP_KEYSERVER+1} ]]; then [[ ${VERIFY_SIG_METHOD} == openpgp ]] || die "${FUNCNAME}: VERIFY_SIG_OPENPGP_KEYSERVER is not supported" extra_args+=( --keyserver "${VERIFY_SIG_OPENPGP_KEYSERVER}" ) fi # GPG upstream knows better than to follow the spec, so we can't # override this directory. However, there is a clean fallback # to GNUPGHOME. addpredict /run/user local filename=${file##*/} [[ ${file} == - ]] && filename='(stdin)' einfo "Verifying ${filename} ..." case ${VERIFY_SIG_METHOD} in minisig) minisign -V -P "$(<"${key}")" -x "${sig}" -m "${file}" || die "minisig signature verification failed" ;; openpgp) # gpg can't handle very long TMPDIR # https://bugs.gentoo.org/854492 local -x TMPDIR=/tmp gemato openpgp-verify-detached -K "${key}" \ "${extra_args[@]}" --no-require-all-good \ "${sig}" "${file}" || die "PGP signature verification failed" ;; signify) signify -V -p "${key}" -m "${file}" -x "${sig}" || die "Signify signature verification failed" ;; esac } # @FUNCTION: verify-sig_verify_message # @USAGE: [] # @DESCRIPTION: # Verify that the file ('-' for stdin) contains a valid, signed PGP # message and write the message into ('-' for stdout). # can either be passed directly, or it defaults # to VERIFY_SIG_OPENPGP_KEY_PATH. The function dies if verification # fails. Note that using output from is important as it # prevents the injection of unsigned data. verify-sig_verify_message() { local file=${1} local output_file=${2} local key=${3} if [[ -z ${key} ]]; then if [[ -z ${VERIFY_SIG_OPENPGP_KEY_PATH} ]]; then die "${FUNCNAME}: no key passed and VERIFY_SIG_OPENPGP_KEY_PATH unset" else key="${BROOT}${VERIFY_SIG_OPENPGP_KEY_PATH}" fi fi local extra_args=() [[ ${VERIFY_SIG_OPENPGP_KEY_REFRESH} == yes ]] || extra_args+=( -R ) if [[ -n ${VERIFY_SIG_OPENPGP_KEYSERVER+1} ]]; then [[ ${VERIFY_SIG_METHOD} == openpgp ]] || die "${FUNCNAME}: VERIFY_SIG_OPENPGP_KEYSERVER is not supported" extra_args+=( --keyserver "${VERIFY_SIG_OPENPGP_KEYSERVER}" ) fi # GPG upstream knows better than to follow the spec, so we can't # override this directory. However, there is a clean fallback # to GNUPGHOME. addpredict /run/user local filename=${file##*/} [[ ${file} == - ]] && filename='(stdin)' einfo "Verifying ${filename} ..." case ${VERIFY_SIG_METHOD} in minisig) minisign -V -P "$(<"${key}")" -x "${sig}" -o "${output_file}" -m "${file}" || die "minisig signature verification failed" ;; openpgp) # gpg can't handle very long TMPDIR # https://bugs.gentoo.org/854492 local -x TMPDIR=/tmp gemato gpg-wrap -K "${key}" "${extra_args[@]}" -- \ gpg --verify --output="${output_file}" "${file}" || die "PGP signature verification failed" ;; signify) signify -V -e -p "${key}" -m "${output_file}" -x "${file}" || die "Signify signature verification failed" ;; esac } # @FUNCTION: verify-sig_verify_unsigned_checksums # @USAGE: # @DESCRIPTION: # Verify the checksums for all files listed in the space-separated list # (akin to ${A}) using a . specifies # the checksum file format. can be "-" for stdin. # # The following formats are supported: # - sha256 -- sha256sum ( ) # - openssl-dgst -- openssl dgst (()=) # # The function dies if one of the files does not match checksums or # is missing from the checksum file. # # Note that this function itself can only verify integrity of the files. # In order to verify their authenticity, the must # be verified against a signature first, e.g. using # verify-sig_verify_detached. If it contains inline signature, use # verify-sig_verify_signed_checksums instead. verify-sig_verify_unsigned_checksums() { local checksum_file=${1} local format=${2} local files=() read -r -d '' -a files <<<"${3}" local chksum_prog chksum_len algo=${format} case ${format} in sha256) chksum_len=64 ;; openssl-dgst) ;; *) die "${FUNCNAME}: unknown checksum format ${format}" ;; esac [[ ${checksum_file} == - ]] && checksum_file=/dev/stdin local line checksum filename junk ret=0 count=0 local -A verified while read -r line; do if [[ ${line} == "-----BEGIN"* ]]; then die "${FUNCNAME}: PGP armor found, use verify-sig_verify_signed_checksums instead" fi case ${format} in sha256) read -r checksum filename junk <<<"${line}" [[ ${#checksum} -ne ${chksum_len} ]] && continue [[ -n ${checksum//[0-9a-f]} ]] && continue [[ -n ${junk} ]] && continue ;; openssl-dgst) [[ ${line} != *"("*")="* ]] && continue checksum=${line##*)=} algo=${line%%(*} filename=${line#*(} filename=${filename%)=*} ;; esac if ! has "${filename}" "${files[@]}"; then continue fi if "${algo,,}sum" -c --strict - <<<"${checksum} ${filename}"; then verified["${filename}"]=1 else ret=1 fi done < "${checksum_file}" [[ ${ret} -eq 0 ]] || die "${FUNCNAME}: at least one file did not verify successfully" [[ ${#verified[@]} -eq ${#files[@]} ]] || die "${FUNCNAME}: checksums for some of the specified files were missing" } # @FUNCTION: _gpg_verify_signed_checksums # @INTERNAL # @USAGE: [] # @DESCRIPTION: # GnuPG-specific function to verify a signed checksums list. _gpg_verify_signed_checksums() { local checksum_file=${1} local algo=${2} local files=${3} local key=${4} verify-sig_verify_unsigned_checksums - "${algo}" "${files}" < <( verify-sig_verify_message "${checksum_file}" - "${key}" ) } # @FUNCTION: verify-sig_verify_signed_checksums # @USAGE: [] # @DESCRIPTION: # Verify the checksums for all files listed in the space-separated list # (akin to ${A}) using a signed . specifies # the checksum algorithm (e.g. sha256). can either be passed # directly, or it defaults to VERIFY_SIG_OPENPGP_KEY_PATH. # # The function dies if signature verification fails, the checksum file # contains unsigned data, one of the files do not match checksums or # are missing from the checksum file. verify-sig_verify_signed_checksums() { local checksum_file=${1} local algo=${2} local files=() read -r -d '' -a files <<<"${3}" local key=${4} if [[ -z ${key} ]]; then if [[ -z ${VERIFY_SIG_OPENPGP_KEY_PATH} ]]; then die "${FUNCNAME}: no key passed and VERIFY_SIG_OPENPGP_KEY_PATH unset" else key="${BROOT}${VERIFY_SIG_OPENPGP_KEY_PATH}" fi fi case ${VERIFY_SIG_METHOD} in openpgp) _gpg_verify_signed_checksums \ "${checksum_file}" "${algo}" "${files[@]}" "${key}" ;; signify) signify -C -p "${key}" \ -x "${checksum_file}" "${files[@]}" || die "Signify signature verification failed" ;; esac } # @FUNCTION: verify-sig_src_unpack # @DESCRIPTION: # Default src_unpack override that verifies signatures for all # distfiles if 'verify-sig' flag is enabled. The function dies if any # of the signatures fails to verify or if any distfiles are not signed. # Please write src_unpack() yourself if you need to perform partial # verification. verify-sig_src_unpack() { if use verify-sig; then local f suffix found local distfiles=() signatures=() nosigfound=() straysigs=() # find all distfiles and signatures, and combine them for f in ${A}; do found= for suffix in .asc .sig .minisig; do if [[ ${f} == *${suffix} ]]; then signatures+=( "${f}" ) found=sig break else if has "${f}${suffix}" ${A}; then distfiles+=( "${f}" ) found=dist+sig break fi fi done if [[ ! ${found} ]]; then nosigfound+=( "${f}" ) fi done # check if all distfiles are signed if [[ ${#nosigfound[@]} -gt 0 ]]; then eerror "The following distfiles lack detached signatures:" for f in "${nosigfound[@]}"; do eerror " ${f}" done die "Unsigned distfiles found" fi # check if there are no stray signatures for f in "${signatures[@]}"; do if ! has "${f%.*}" "${distfiles[@]}"; then straysigs+=( "${f}" ) fi done if [[ ${#straysigs[@]} -gt 0 ]]; then eerror "The following signatures do not match any distfiles:" for f in "${straysigs[@]}"; do eerror " ${f}" done die "Unused signatures found" fi # now perform the verification for f in "${signatures[@]}"; do verify-sig_verify_detached \ "${DISTDIR}/${f%.*}" "${DISTDIR}/${f}" done fi # finally, unpack the distfiles default_src_unpack } fi EXPORT_FUNCTIONS src_unpack