#!/usr/bin/bash # Copyright 2018 – 2024 B. Persson, Bjorn@Rombobeorn.se # # This material is provided as is, with absolutely no warranty expressed # or implied. Any use is at your own risk. # # Permission is hereby granted to use or copy this program # for any purpose, provided the above notices are retained on all copies. # Permission to modify the code and to distribute modified code is granted, # provided the above notices are retained, and a notice that the code was # modified is included with the above copyright notice. function print_help { cat <<'EOF' gpgverify is a wrapper around gpgv designed for easy and safe scripting. It verifies a file against an OpenPGP signature and one or more keyrings. The keyrings shall together contain all the keys that are trusted to certify the authenticity of the file, and must not contain any untrusted keys. To verify a detached signature, where the signature and the signed data are two separate files, both --signature and --data must be given: gpgverify --keyring= --signature= --data= gpgverify --keyrings ... --signature= --data= If the signature is embedded in the signed file, give --data and --output: gpgverify --keyring= --data= --output= gpgverify --keyrings ... --data= --output= The verified data will be written to the output file. An output file is required even for a clearsigned text file, because a clearsigned block can be surrounded by unsigned text. A program that trusts the contents of a clearsigned file after verifying the signature is vulnerable to spoofing. Only the contents of the output file can be trusted. The differences, compared to invoking gpgv directly, are that gpgverify accepts keyrings in either ASCII-armored or unarmored form, that it won't accidentally use a default keyring in addition to the specified ones, and that it insists on writing an output file if the signature is not detached. Parameters: --keyring= keyring with only trusted keys (can be repeated) --keyrings Multiple keyrings with only trusted keys follow. --signature= detached signature to verify --data= signed file to verify, or data file to verify against a detached signature --output= file to write the verified data to when the signature is embedded in the signed file EOF } function fatal_error { message="$1" # an error message status=$2 # a number to use as the exit code echo "gpgverify: $message" >&2 exit $status } function parameter_error { message="$1" # an error message fatal_error "${message}" 2 } function require_parameter { term="$1" # a term for a required parameter value="$2" # Complain and terminate if this value is empty. if test -z "${value}" ; then parameter_error "No ${term} was provided." fi } function check_status { action="$1" # a string that describes the action that was attempted status=$2 # the exit code of the command if test $status -ne 0 ; then fatal_error "$action failed." $status fi } # Parse the command line. keyring_parameters=false keyrings=() # empty array signature= data= output= for parameter in "$@" ; do if [[ "${parameter}" = -* ]] ; then # This parameter begins with a dash, so it's not part of any list of # keyrings. keyring_parameters=false fi case "${parameter}" in (--help) print_help exit ;; (--keyrings) # The following parameters will be keyring pathnames until one # begins with a dash. keyring_parameters=true ;; (--keyring=*) keyrings+=("${parameter#*=}") ;; (--signature=*) if test -n "${signature}" ; then # This is a second occurrence of --signature. parameter_error 'Only one signature at a time can be verified.' fi signature="${parameter#*=}" ;; (--data=*) if test -n "${data}" ; then # This is a second occurrence of --data. parameter_error 'Only one data file at a time can be verified.' fi data="${parameter#*=}" ;; (--output=*) if test -n "${output}" ; then # This is a second occurrence of --output. parameter_error 'Only one output file can be written.' fi output="${parameter#*=}" ;; (*) if ${keyring_parameters} ; then keyrings+=("${parameter}") else parameter_error "Unknown parameter: \"${parameter}\"" fi ;; esac done require_parameter 'keyring' "${keyrings}" require_parameter 'data file' "${data}" # If no detached signature is provided, then the signature is embedded in the # data file, and there must be an output file to write the verified data to – # even if the data file is clearsigned. if test -z "${signature}${output}" ; then msg='Neither a signature nor an output file was provided. ' msg+='Trusting a clearsigned file without stripping off unsigned text ' msg+='makes you vulnerable to spoofing.' parameter_error "${msg}" fi # Make a temporary working directory. workdir="$(mktemp --directory)" check_status 'Making a temporary directory' $? # Decode any ASCII armor on the keyrings. keyring_params=() # empty array number=1 for keyring in "${keyrings[@]}" ; do if grep --quiet '^-----BEGIN PGP PUBLIC KEY BLOCK-----' "${keyring}" ; then # This looks like an ASCII-armored keyring. ring="${workdir}/keyring${number}.gpg" gpg2 --homedir="${workdir}" --yes --output="${ring}" --dearmor \ "${keyring}" check_status "Decoding the keyring \"${keyring}\"" $? ((++number)) else # This is not an ASCII-armored keyring. Don't dearmor it, but ensure # that the pathname contains slashes to prevent GnuPG from looking for # the file in the wrong place. ring=`realpath "${keyring}" --canonicalize-existing` check_status 'Accessing a keyring' $? fi keyring_params+=("--keyring=${ring}") done # Verify the signature using the decoded keyrings. # The signature pathname shall be a single parameter even if it contains # whitespace, but shall be omitted entirely if it's an empty string. gpgv2 --homedir="${workdir}" "${keyring_params[@]}" \ ${output:+"--output=${output}"} ${signature:+"${signature}"} "${data}" check_status 'Signature verification' $? # (--homedir isn't actually necessary. --dearmor processes only the input file, # and if --keyring is used and contains a slash, then gpgv2 uses only that # keyring. Thus neither command will look for a default keyring, but --homedir # makes extra double sure that no default keyring will be touched in case # another version of GPG works differently.) # Clean up. (This is not done in case of an error that may need inspection.) rm --recursive --force ${workdir}