Using gpg/pgp signatures in Package Managers
The previous thread focused more on the how and why, we should use various tools to validate data. I would like here to flesh out some draft implementaiton details that I want to apply to my fork of Scotmann's package manager (aka package).
This code is currently untested because it is in a draft state but writing this will help me know where to continue tomorrow.
In the debian/apt sources.list one can specify extra options in square. For example one can have [1]:
Code: Select all
deb [signed-by=6809F110790E0720856B562F744ACF4DF3319FFA] https://deriv.example.net/debian/ stable main
In the above case the value assigned to signed-by is the pgp fingerprint of a public key. A fingerprint is generated by taking a cryptographic hash to the public key [2] and it uniquely identifies the public key. If you specify a fingerprint of the public key then unless you use the exclamation mark character (i.e. "!") all subkeys will be considered valid keys for signing [3] . My guess is a subkey is a key that is signed by said key and hasen't yet been revoked or expired.
As an alternative to specifying the fingerprint of the public key you can specify a path to a keyring. In this example rather than using the sources.list format we will use the DEB822-STYLE format [1][3]:
Code: Select all
Types: deb deb-src
URIs: https://deriv.example.net/debian/
Suites: stable
Architectures: i386 amd64
Components: main
Signed-By: /usr/share/keyrings/deriv-archive-keyring.gpg
The DEB822-STYLE is not yet implemented in either sc0ttman's official version of pkg or my fork. In my fork I'm focusing on the sources.list style first.
So some preliminary untested code
The first thing we want to do is parse the options from the sources.list file. In the current official version of pkg, the extra sources.list options are stripped out:
Code: Select all
local apt_sources_list="$(grep -h '^deb ' /etc/apt/sources.list /etc/apt/sources.list.d/*.list 2>/dev/null \
| sed \
-e "s/^deb //g" \
-e "s/^tor+//g" \
-e 's/\[arch=[a-z,0-9].*\] //g' \
-e 's/ /|/g'\
)"
and also the code only assumes that there is at most one extra option (i.e. "arch"). However, there are actually quite a bit more possibilities [3] such as: Languages, Targets, PDiffs, By-Hash, Allow-Insecure, etc...
So first we must parse out these options before we strip them with something like
Code: Select all
"sed -e 's/\[arch=[a-z,0-9].*\] //g' \"
The tor flag is okay to remove because we should be able to tell that we want to use tor by the .onion domain. In the official pkg code the empty space character is replaced with the pipe character so that the "for in" notation can be used. This avoid process substitution which isn't supported by ash. The official version of package uses ash but my fork uses bash.
In my draft code the options are parsed out as follows (draft untested code):
Code: Select all
if [ ${line:0:1} = "[" ]; then
local ind=`expr index "$line" "]"`
local ind2=$((ind - 1))
local line_opts=${line:1:$ind2}
else
local line_opts=''
fi
This is somewhat redundant because in a function that gets called later this will be done if it hasn't been done already (more untested code):
Code: Select all
parse_sources_list_opts(){
local line="${1/#deb /}" #Remove
if [ ${line:0:1} = "[" ]; then
local ind=`expr index "$line" "]"`
local ind2=$((ind - 1))
local line=${line:1:$ind2}
fi
set -- $line
for opt in "$@"; do
opt="$(echo $opt | tr "-" "_")"
echo "export SOURCES_LIST_OPT__$opt"
done
}
What we are doing here is we are prepending SOURCES_LIST_OPT__ to these extra sources.list options and then exporting the variables so that they can be used in ppa2pup.
ppa2pup is called as follows:
Code: Select all
(
if [ ! -z "$line_opts" ]; then
eval parse_sources_list_opts "$line_opts"
fi
ppa2pup ${line//|/ } 1>/dev/null && echo -e "${green}Success${endcolour}: Updated repo '$ppa_repo_name'"
)
The metadata for the repo is located at:
Code: Select all
URL=$(echo $1 | sed -e 's#/$##g')/dists/${distro_ver}/${repo_stream}/binary-${arch}/Packages.gz
Later in the code we will remove the last part of this url. The last part of the url is:
Code: Select all
ARCH_PATH="/binary-${arch}/Packages.gz"
and what we are left with is the URL to the directory where the metadata about the release is located:
Code: Select all
[url]meta_url=${download_url//$ARCH_PATH/}[/url]
This either consists of two files:
Release and Release.gpg
Example: debian-stretch
or a single file which has the pgp signing information embedded in the file:
InRlease
Example: Tor-Devian-stretch
In my draft code I first try to download Release and Release.pgp and if this fails I try to download InRelease and then verify the signature:
Code: Select all
meta_download_failed=false
wget --quiet "${meta_url}/Release" -O "$RELEASE_DIR/$repo_name/Release" 1>/dev/null && \
wget --quiet"${meta_url}/Release.gpg" -O "$RELEASE_DIR/$repo_name/Release.gpg" 1>/dev/null || \
meta_download_failed=true
if [ "$meta_download_failed" = false ]; then
if ( cd $RELEASE_DIR/$repo_name;
$PGP_VERIFY_CMD --keyring "$SOURCES_LIST_OPT__signed_by" Release.pgp Release ); then
pgp_verified=maybe
ReleaseFile="$RELEASE_DIR/$repo_name/InRelease"
else
pgp_verified=false
fi
else
meta_download_failed=false
wget --quiet "${meta_url}/InRelease" -O "$RELEASE_DIR/$repo_name/InRelease" 1>/dev/null \
|| meta_download_failed=true
if [ "$meta_download_failed" = false ]; then
if ( cd $RELEASE_DIR/$repo_name;
$PGP_VERIFY_CMD --keyring "$SOURCES_LIST_OPT__signed_by" InRelease ); then
pgp_verified=maybe
ReleaseFile="$RELEASE_DIR/$repo_name/InRelease"
else
pgp_verified=false
fi
fi
fi
if [ "$pgp_verified" = false ]; then
if [ ! -z "$SOURCES_LIST_OPT__Trusted" ] && [ "$SOURCES_LIST_OPT__Trusted" = yes ]; then
FAIL_CLASS=WARNING
else
FAIL_CLASS=ERROR
fi
echo
echo "$FAIL_CLASS: could not verify pgp signature of Release/InRelease file"
echo
echo " ${meta_url}"
if [ ! "$SOURCES_LIST_OPT__Trusted" ]; then #Maybe should exit if this
exit 1
fi
fi
If the signature is valid then we check the hash of the metadata (i.e. Packages.gz)
Example Tor (Uncomressed): Packages
Note that I believe that the hash is of the uncompressed data (to verify in testing).
Anyway, once the hash is computed of the metadata then we look in the release file to see if we can find a matching hash. Extra options in sources.list specify how strong a hash is required:
Code: Select all
if [ SOURCES_LIST_OPT__Allow_Insecure = yes ]; then
hash_function=( sha256sum md5sum sha1sum none )
elif [ "$SOURCES_LIST_OPT__allow_weak" = yes ]; then
hash_function=( sha256sum md5sum sha1sum )
else
hash_function=( sha256sum )
fi
We now try each hash starting from the strongest to the weakest (or none in the insecure case) and fail if we can't verify the metadata with a strong enough hash:
Code: Select all
for a_hash_fn in "${hash_function[@]}"; then
case "$a_hash_fn" in
sha256sum)
fsum="$(sha256sum "$file_path" | cut -f1 -d' ')"
if [ $(grep -m -c "$fsum" "$ReleaseFile" ) -gt 0 ]; then
break
fi ;;
md5sum)
fsum="$(md5sum "$file_path" | cut -f1 -d' ')"
if [ $(grep -m -c "$fsum" "$ReleaseFile" ) -gt 0 ]; then
echo "WARNING: weak checksum for $repo_name"
break
fi ;;
sha1sum)
fsum="$(sha1sum "$file_path" | cut -f1 -d' ')"
if [ $(grep -m -c "$fsum" "$ReleaseFile" ) -gt 0 ]; then
echo "WARNING: weak checksum for $repo_name"
break
fi ;;
none)
fsum="$(md5sum "$file_path" | cut -f1 -d' ')"
echo "WARNING: repo metadata unverified"
break ;;
fi
fi
One thing that I haven't tried to implement yet is looking inside the .dep file for signed hashes if I'm not able to get this info from the repo metadata. See:
https://blog.packagecloud.io/eng/2014/1 ... ositories/
this approach might not be as secure though. See:
https://www.chosenplaintext.ca/articles ... ashes.html
Notes
------------------
1 - https://wiki.debian.org/DebianRepository/UseThirdParty
2 - https://en.wikipedia.org/wiki/Public_key_fingerprint
3 - https://manpages.debian.org/buster/apt/ ... .5.en.html