├── LICENSE ├── CONTRIBUTING ├── .github └── ISSUE_TEMPLATE │ └── metadata-issue.md ├── CONTRIBUTING.md ├── repo-mirror.sh ├── repo-rpm.sh ├── repo-debian.sh ├── repo-overlay.sh ├── SIGNATURES.md ├── repo-validate.sh ├── README.md └── genmodules.py /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 NVIDIA Corporation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 1 Letterman Drive 6 | Suite D4700 7 | San Francisco, CA, 94129 8 | 9 | Everyone is permitted to copy and distribute verbatim copies of this 10 | license document, but changing it is not allowed. 11 | 12 | 13 | Developer's Certificate of Origin 1.1 14 | 15 | By making a contribution to this project, I certify that: 16 | 17 | (a) The contribution was created in whole or in part by me and I 18 | have the right to submit it under the open source license 19 | indicated in the file; or 20 | 21 | (b) The contribution is based upon previous work that, to the best 22 | of my knowledge, is covered under an appropriate open source 23 | license and I have the right under that license to submit that 24 | work with modifications, whether created in whole or in part 25 | by me, under the same open source license (unless I am 26 | permitted to submit under a different license), as indicated 27 | in the file; or 28 | 29 | (c) The contribution was provided directly to me by some other 30 | person who certified (a), (b) or (c) and I have not modified 31 | it. 32 | 33 | (d) I understand and agree that this project and the contribution 34 | are public and that a record of the contribution (including all 35 | personal information I submit with it, including my sign-off) is 36 | maintained indefinitely and may be redistributed consistent with 37 | this project or the open source license(s) involved. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/metadata-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Metadata issue 3 | about: Reporting metadata issues with the CUDA repositories 4 | title: 'Metadata issue with the CUDA repositories [CDN] ' 5 | labels: '' 6 | assignees: kmittman 7 | 8 | --- 9 | 10 | ## Reporting metadata issues with the CUDA repositories 11 | 12 | Is the CDN stale? Have you seen something like: 13 | ```shell 14 | E: Failed to fetch *.deb Hash Sum mismatch 15 | Hashes of expected file: 16 | - SHA512:$sha512, SHA256:$sha256, SHA1:$sha1 [weak], MD5Sum:$md5 [weak], Filesize:$bytes [weak] 17 | Hashes of received file: 18 | - SHA512:$sha512, SHA256:$sha256, SHA1:$sha1 [weak], MD5Sum:$md5 [weak], Filesize:$bytes [weak] 19 | Last modification reported: $(date -R --utc) 20 | ``` 21 | 22 | or 23 | 24 | ```shell 25 | *.rpm: Downloading successful, but checksum doesn't match. 26 | Calculated: $sha256(sha256) 27 | Expected: $sha256(sha256) 28 | ``` 29 | 30 | ### Please provide the following information in your comment: 31 | 32 | 1. The error message and the last command(s) run. 33 | 34 | 2. When was the `Release` (Debian) or `repomd.xml` (RPM) file last modified ? 35 | ```shell 36 | $ curl -I https://developer.download.nvidia.com/compute/cuda/repos/$distro/$arch/Release 37 | $ curl -I https://developer.download.nvidia.com/compute/cuda/repos/$distro/$arch/repodata/repomd.xml 38 | ``` 39 | 40 | 3. The Linux distro and architecture. If cross-compiling or containerized, please mention that. 41 | ```shell 42 | $ cat /etc/os-release 43 | $ uname -a 44 | ``` 45 | 46 | 4. Which NVIDIA repositories do you have enabled ? 47 | Do your `.list` / `.repo` files contain URLs using HTTP (port 80) or HTTPS (port 443) ? 48 | 49 | 5. Which geographic region is the machine located in ? 50 | 51 | 6. Which CDN edge node are you hitting ? 52 | 53 | 7. Any other relevant environmental conditions (i.e. a specific Docker container image) ? 54 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute to this project 2 | 3 | Want to contribute to this project? Awesome! 4 | We only require you to sign your work, the below section describes this! 5 | 6 | ## Sign your work 7 | 8 | The sign-off is a simple line at the end of the explanation for the patch. Your 9 | signature certifies that you wrote the patch or otherwise have the right to pass 10 | it on as an open-source patch. The rules are pretty simple: if you can certify 11 | the below (from [developercertificate.org](http://developercertificate.org/)): 12 | 13 | ``` 14 | Developer Certificate of Origin 15 | Version 1.1 16 | 17 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 18 | 1 Letterman Drive 19 | Suite D4700 20 | San Francisco, CA, 94129 21 | 22 | Everyone is permitted to copy and distribute verbatim copies of this 23 | license document, but changing it is not allowed. 24 | 25 | Developer's Certificate of Origin 1.1 26 | 27 | By making a contribution to this project, I certify that: 28 | 29 | (a) The contribution was created in whole or in part by me and I 30 | have the right to submit it under the open source license 31 | indicated in the file; or 32 | 33 | (b) The contribution is based upon previous work that, to the best 34 | of my knowledge, is covered under an appropriate open source 35 | license and I have the right under that license to submit that 36 | work with modifications, whether created in whole or in part 37 | by me, under the same open source license (unless I am 38 | permitted to submit under a different license), as indicated 39 | in the file; or 40 | 41 | (c) The contribution was provided directly to me by some other 42 | person who certified (a), (b) or (c) and I have not modified 43 | it. 44 | 45 | (d) I understand and agree that this project and the contribution 46 | are public and that a record of the contribution (including all 47 | personal information I submit with it, including my sign-off) is 48 | maintained indefinitely and may be redistributed consistent with 49 | this project or the open source license(s) involved. 50 | ``` 51 | 52 | Then you just add a line to every git commit message: 53 | 54 | Signed-off-by: Joe Smith 55 | 56 | Use your real name (sorry, no pseudonyms or anonymous contributions.) 57 | 58 | If you set your `user.name` and `user.email` git configs, you can sign your 59 | commit automatically with `git commit -s`. 60 | -------------------------------------------------------------------------------- /repo-mirror.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC2001,SC2068,SC2086 3 | # Copyright 2021, NVIDIA Corporation 4 | # SPDX-License-Identifier: MIT 5 | 6 | outputDir="$HOME/mirror" 7 | baseURL="https://developer.download.nvidia.com/compute/cuda/repos" 8 | version=("") 9 | 10 | packages=1 11 | metadata=1 12 | directory=0 13 | 14 | err() { echo "ERROR: $*"; exit 1; } 15 | 16 | usage() { 17 | echo "USAGE: $0 [output] [version] [url]" 18 | echo 19 | echo " PARAMETERS:" 20 | echo -e " --distro=[rhel8,ubuntu2004]\t Linux distro name" 21 | echo -e " --arch=[x86_64,ppc64le,sbsa]\t CPU architecture" 22 | echo 23 | echo " OPTIONAL:" 24 | echo -e " --dryrun\t\t skip downloading files" 25 | echo -e " --follow\t\t also download files in subdirectories" 26 | echo -e " --metadata\t\t only download metadata files" 27 | echo -e " --packages\t\t only download package files" 28 | echo -e " --output=\t save directory" 29 | echo -e " --version=<11.4.2>\t filter results by string" 30 | echo -e " --url=\t\t override base URL to repository" 31 | echo 32 | err "$*" 33 | } 34 | 35 | 36 | file_downloader() { 37 | local inputURL="$1" 38 | local outFile="$2" 39 | curl -sL "$inputURL" --output "$outFile" -A "$(basename $0)" 40 | } 41 | 42 | precheck_files() 43 | { 44 | stored=0 45 | queued=0 46 | # Pre-check packages 47 | for file in $@; do 48 | if [[ -f ${outputDir}/${distro}/${arch}/${file} ]]; then 49 | stored=$((stored + 1)) 50 | else 51 | queued=$((queued + 1)) 52 | fi 53 | done 54 | 55 | if [[ $stored -gt 0 ]]; then 56 | echo ":: Found $stored local files" 57 | fi 58 | } 59 | 60 | download_files() 61 | { 62 | precheck_files $@ 63 | 64 | if [[ $queued -gt 0 ]]; then 65 | echo ":: Downloading $queued repo files" 66 | else 67 | echo ":: Up-to-date" 68 | return 69 | fi 70 | 71 | # Download packages 72 | for file in $@; do 73 | if [[ -f ${outputDir}/${distro}/${arch}/${file} ]]; then 74 | echo "[SKIP] $file" 75 | elif [[ -n $dryrun ]]; then 76 | echo " -> $file" 77 | dirname=$(dirname "$file") 78 | mkdir -p "${outputDir}/${distro}/${arch}/${dirname}" 79 | touch "${outputDir}/${distro}/${arch}/${file}" 80 | else 81 | echo " -> $file" 82 | dirname=$(dirname "$file") 83 | mkdir -p "${outputDir}/${distro}/${arch}/${dirname}" 84 | file_downloader "${baseURL}/${distro}/${arch}/${file}" "${outputDir}/${distro}/${arch}/${file}" 85 | fi 86 | done 87 | } 88 | 89 | 90 | meta_debs() 91 | { 92 | repoDEB=("Release" "Release.gpg" "Packages" "Packages.gz") 93 | metaFiles=$(echo ${repoDEB[@]} | sed 's| |\n|g') 94 | } 95 | 96 | copy_debs() 97 | { 98 | meta_debs 99 | gzipPath=$(echo "$metaFiles" | grep "\.gz") 100 | 101 | echo "==> Parsing $gzipPath" 102 | textBlocks=$(curl -sL "${baseURL}/${distro}/${arch}/${gzipPath}" --output - | gunzip -c -) 103 | packageFiles=$(echo "$textBlocks" | grep -E "$matching" | grep "^Filename: " | awk -F './' '{print $2}') 104 | 105 | if [[ -z "$packageFiles" ]]; then 106 | err "Unable to locate packages in repository for ${distro}/${arch}" 107 | fi 108 | 109 | download_files $packageFiles 110 | } 111 | 112 | meta_rpms() 113 | { 114 | repoMD=$(curl -sL "${baseURL}/${distro}/${arch}/repodata/repomd.xml") 115 | metaFiles=$(echo "$repoMD" | grep "href=" | awk -F '"' '{print $2}' | sed '1irepodata/repomd.xml') 116 | } 117 | 118 | copy_rpms() 119 | { 120 | meta_rpms 121 | gzipPath=$(echo "$metaFiles" | grep primary\.xml) 122 | 123 | echo "==> Parsing $gzipPath" 124 | primaryXML=$(curl -sL "${baseURL}/${distro}/${arch}/${gzipPath}" --output - | gunzip -c -) 125 | packageFiles=$(echo "$primaryXML" | grep -E "$matching" | grep " Parsing $folder" 139 | dirLinks=$(echo "$dirHTML" | sed 's|><|>\n<|g' | grep " Parsing index" 153 | linkTags=$(echo "$indexHTML" | sed 's|><|>\n<|g' | grep " [workdir] [gpgkey]" 20 | echo 21 | echo " PARAMETERS:" 22 | echo -e " --input=\t overlay with changes" 23 | echo -e " --mirror=\t source of truth" 24 | echo -e " --repo=\t \$distro/\$arch to traverse" 25 | echo 26 | echo " OPTIONAL:" 27 | echo -e " --nocache\t\t rebuild metadata" 28 | echo -e " --gpgkey=\t shortname for GPG signing keypair" 29 | echo -e " --workdir=\t scratch area for temp files" 30 | echo 31 | err "$*" 32 | } 33 | 34 | compare_file() { 35 | local file1=$(md5sum "$mirror/$2/$3" | awk '{print $1}') 36 | local file2=$(md5sum "$1/$2/$3" | awk '{print $1}') 37 | echo " -> [file1] $mirror/$2/$3: $file1" 38 | echo " -> [file2] $1/$2/$3: $file2" 39 | if [[ "$file1" == "$file2" ]]; then 40 | echo ":: files are identical" 41 | return 0 42 | else 43 | echo ":: files are different" 44 | return 1 45 | fi 46 | } 47 | 48 | get_checksum() { 49 | local dir="$1" 50 | md5sum "$dir"/* | sed 's|\/| |g' | awk '{print $1,$NF}' | sort -k2 51 | } 52 | 53 | rpm_md5sum() { 54 | local parent="$1" 55 | local subpath="$2" 56 | 57 | rpms=$(find "$parent/$subpath" -mindepth 1 -maxdepth 1 -type d -name "repodata" 2>/dev/null | sort) 58 | for rpm_repo in $rpms; do 59 | echo "==> $rpm_repo" 60 | get_checksum "$rpm_repo" 61 | done 62 | } 63 | 64 | compare_rpm_md5sum() { 65 | [[ -d "$1" ]] || err "USAGE: compare_rpm_md5sum() [dir2] [subpath]" 66 | [[ -d "$2" ]] || err "USAGE: compare_rpm_md5sum() [dir1] [subpath]" 67 | [[ -n "$3" ]] || err "USAGE: compare_rpm_md5sum() [dir1] [dir2] " 68 | 69 | file1=$(rpm_md5sum "$1" "$3") 70 | file2=$(rpm_md5sum "$2" "$3") 71 | 72 | echo "$file1" 73 | echo "---" 74 | echo "$file2" 75 | echo "---------" 76 | 77 | two_way=$(comm -1 -3 <(echo "$file1" | sort) <(echo "$file2" | sort) | grep -v "^==>" | sort -k2) 78 | echo "$two_way" 79 | 80 | for line in $(echo "$two_way" | awk '{print $2}'); do 81 | echo "${subpath}/repodata/${line}" >> "$fileManifest" 82 | done 83 | 84 | diff_count=$(echo "$two_way" | wc -l) 85 | [[ $diff_count -gt 1 ]] || err "metadata unchanged" 86 | echo ":: $diff_count metadata file(s) added or modified" 87 | } 88 | 89 | check_modular() { 90 | local distnum=$(echo "$distro" | tr -dc '0-9\n') 91 | 92 | if [[ "$distro" =~ "rhel" ]] && [[ "$distnum" -ge 8 ]]; then 93 | echo "Detected RHEL >= 8, turning on modularity" 94 | modular=1 95 | elif [[ "$distro" =~ "fedora" ]] && [[ "$distnum" -ge 28 ]]; then 96 | echo "Detected Fedora >= 28, turning on modularity" 97 | modular=1 98 | elif [[ "$distro" =~ "kylin" ]]; then 99 | echo "Detected Kylin, turning on modularity" 100 | modular=1 101 | else 102 | echo "Non modularity distro detected ($distro : $distnum), keeping modularity off" 103 | return 0 104 | fi 105 | 106 | if [[ -f "$genmodulesLOCAL" ]]; then 107 | genmodules="$genmodulesLOCAL" 108 | elif [[ -f "$genmodulesPARENT" ]]; then 109 | genmodules="$genmodulesPARENT" 110 | elif [[ -n "$genmodulesPATH" ]]; then 111 | genmodules="$genmodulesPATH" 112 | elif [[ -z "$remoteModules" ]] && [[ -z "$localModules" ]]; then 113 | echo 114 | echo ":: Skipping modularity, no $moduleName packages found" 115 | echo 116 | elif [[ -n "$moduleName" ]]; then 117 | echo 118 | echo ">>> [$moduleName] modularity" 119 | echo "NOTICE: fetch genmodules.py script from https://github.com/NVIDIA/yum-packaging-precompiled-kmod" 120 | err "unable to locate 'genmodules.py' in $current or \$PATH" 121 | fi 122 | } 123 | 124 | rpm_modularity() { 125 | local rpmdir="$1" 126 | 127 | if [[ $modular -ne 1 ]]; then 128 | return 129 | fi 130 | 131 | echo "Running modularity with (remoteModules = $remoteModules) (localModules = $localModules)" 132 | # Driver streams expect driver packages present 133 | if [[ -n "$remoteModules" ]] || [[ -n "$localModules" ]]; then 134 | echo "%%%%%%%%%%%%%%%%%%" 135 | echo "%%% Modularity %%%" 136 | echo "%%%%%%%%%%%%%%%%%%" 137 | 138 | echo ">>> python3 $genmodules $rpmdir $rpmdir/modules.yaml" 139 | python3 $genmodules "$rpmdir" ${rpmdir}/modules.yaml || err "./genmodules.py $rpmdir $rpmdir/modules.yaml" 140 | [[ -f "${rpmdir}/modules.yaml" ]] || err "modules.yaml not found at $rpmdir" 141 | echo 142 | 143 | echo ">>> modifyrepo_c modules.yaml $rpmdir/repodata" 144 | modifyrepo_c ${rpmdir}/modules.yaml $rpmdir/repodata || err "modifyrepo_c ${rpmdir}/modules.yaml ${rpmdir}/repodata" 145 | echo 146 | else 147 | echo "Skipping modularity as there's no local/remote modules" 148 | fi 149 | } 150 | 151 | rpm_metadata() { 152 | local donor="$1" 153 | local parent="$2" 154 | local subpath="$3" 155 | repomd="repodata/repomd.xml" 156 | oldPWD="$PWD" 157 | 158 | cd "$parent"/"$subpath" || err "unable to cd to $parent / $subpath" 159 | 160 | #FIXME WAR for overlayFS invalid cross-device link 161 | if [[ -n "$DEVMODE" ]]; then 162 | echo "[FIXME] remove old repo metadata files" 163 | elif [[ -f "repodata/repomd.xml" ]]; then 164 | mkdir -p old/repodata 165 | for file in $(grep "href=" "repodata/repomd.xml" 2>/dev/null | awk -F '"' '{print $2}'); do 166 | mv -v "$file" old/repodata 167 | done 168 | mv -v "repodata/repomd.xml" old/repodata/ 169 | rm -rf repodata 170 | #mv old repodata 171 | fi 172 | 173 | # 174 | # Process new or modified RPM packages 175 | # 176 | if [[ -z "$nocache" ]]; then 177 | repoArgs="--update --update-md-path $PWD/old" 178 | # 179 | # Process all RPM packages 180 | # 181 | else 182 | unset repoArgs 183 | fi 184 | 185 | echo ">>> createrepo_c -v --database $repoArgs $PWD" 186 | createrepo_c -v --database $repoArgs "$PWD" 2>&1 | tee "$logFile" 187 | [[ ${PIPESTATUS[0]} -eq 0 ]] || err "createrepo_c failed" 188 | echo 189 | 190 | pkg_cache=$(cat "$logFile" 2>/dev/null | grep "CACHE HIT" | awk '{print $NF}') 191 | pkg_modify=$(cat "$logFile" 2>/dev/null | grep "metadata are obsolete" | awk '{print $2}') 192 | pkg_list=$(find "$PWD" -maxdepth 1 -type f -name "*.rpm" 2>/dev/null | awk -F '/' '{print $NF}') 193 | pkg_diff=$(comm -1 -3 <(echo "$pkg_cache" | sort) <(echo "$pkg_list" | sort)) 194 | for pkg in $(echo -e "${pkg_diff}\n${pkg_modify}" | sort -u); do 195 | echo ":: ${subpath}/${pkg}" 196 | echo "${subpath}/${pkg}" >> "$fileManifest" || err "scratch image too small" 197 | done 198 | echo 199 | 200 | # Modularity 201 | if [[ -n "$modular" ]]; then 202 | rpm_modularity "$PWD" 203 | fi 204 | 205 | echo "==> Sanity check for repomd.xml" 206 | if [[ -n "$mirror" ]] && [[ -f $mirror/$subpath/$repomd ]]; then 207 | compare_file "$parent" "$subpath" "$repomd" && err "expected new metadata" 208 | else 209 | echo " :: Old repo not found" 210 | fi 211 | echo 212 | 213 | # Sign checksum file with key 214 | if [[ $gpgkeyName == "UNSIGNED" ]]; then 215 | echo "==> Skipping signing (use external signing server)" 216 | else 217 | echo ">>> gpg --batch --yes -a -u ${gpgkeyName} --detach-sign --personal-digest-preferences SHA512 $repomd" 218 | gpg --batch --yes -a -u ${gpgkeyName} --detach-sign --personal-digest-preferences SHA512 "$repomd" || err "repomd.xml.asc failed" 219 | echo ">>> gpg --batch --yes -a --export ${gpgkeyName} > ${repomd}.key" 220 | gpg --batch --yes -a --export ${gpgkeyName} > ${repomd}.key || err "repomd.xml.key failed" 221 | echo 222 | fi 223 | 224 | # Preserve old repodata 225 | if [[ -d "old/repodata" ]]; then 226 | mv -v repodata/* old/repodata/ 227 | rmdir repodata 228 | mv old/repodata repodata 229 | rmdir old 230 | fi 231 | 232 | rmdir old 2>/dev/null 233 | cd "$oldPWD" >/dev/null 234 | echo 235 | 236 | if [[ -d "$donor" ]] && [[ -z $nocache ]]; then 237 | compare_rpm_md5sum "$donor" "$inputDir" "$subpath" 238 | fi 239 | echo 240 | } 241 | 242 | 243 | # Options 244 | while [[ $1 =~ ^-- ]]; do 245 | # Full rebuild of metadata 246 | if [[ $1 =~ ^--nocache$ ]] || [[ $1 =~ ^--no-cache$ ]]; then 247 | nocache=1 248 | # Repository relative path 249 | elif [[ $1 =~ "repo=" ]]; then 250 | subpath=$(echo "$1" | awk -F "=" '{print $2}') 251 | elif [[ $1 =~ ^--repo$ ]]; then 252 | shift; subpath="$1" 253 | # Repository architecture 254 | elif [[ $1 =~ "arch=" ]]; then 255 | arch=$(echo "$1" | awk -F "=" '{print $2}') 256 | elif [[ $1 =~ ^--arch$ ]]; then 257 | shift; arch="$1" 258 | # Repository distro name 259 | elif [[ $1 =~ "distro=" ]]; then 260 | distro=$(echo "$1" | awk -F "=" '{print $2}') 261 | elif [[ $1 =~ ^--distro$ ]]; then 262 | shift; distro="$1" 263 | # Scratch directory 264 | elif [[ $1 =~ "workdir=" ]]; then 265 | workDir=$(echo "$1" | awk -F "=" '{print $2}') 266 | elif [[ $1 =~ ^--workdir$ ]]; then 267 | shift; workDir="$1" 268 | # Source of truth 269 | elif [[ $1 =~ "mirror=" ]]; then 270 | mirror=$(echo "$1" | awk -F "=" '{print $2}') 271 | elif [[ $1 =~ ^--mirror$ ]]; then 272 | shift; mirror="$1" 273 | # Release candidate 274 | elif [[ $1 =~ "input=" ]]; then 275 | inputDir=$(echo "$1" | awk -F "=" '{print $2}') 276 | elif [[ $1 =~ ^--input$ ]]; then 277 | shift; inputDir="$1" 278 | # Signing key name 279 | elif [[ $1 =~ "gpg=" ]] || [[ $1 =~ "gpgkey=" ]]; then 280 | gpgkeyName=$(echo "$1" | awk -F "=" '{print $2}') 281 | elif [[ $1 =~ ^--gpg$ ]] || [[ $1 =~ ^--gpgkey$ ]]; then 282 | shift; gpgkeyName="$1" 283 | fi 284 | shift 285 | done 286 | 287 | 288 | [[ -d "$inputDir" ]] || usage "Must specify --input directory (read-write)" 289 | 290 | # Allow --repo parameter to be optional 291 | if [[ -z "$subpath" ]]; then 292 | nestPath=$(basename "$inputDir" 2>/dev/null) 293 | inputDir=$(dirname "$inputDir" 2>/dev/null) 294 | basePath=$(basename "$inputDir" 2>/dev/null) 295 | inputDir=$(dirname "$inputDir" 2>/dev/null) 296 | subpath="${basePath}/${nestPath}" 297 | fi 298 | 299 | # Set default signing key 300 | [[ -n "$gpgkeyName" ]] || gpgkeyName="$publicKey" 301 | 302 | # Temp files 303 | [[ -n "$workDir" ]] || workDir=$(mktemp -d) 304 | [[ -d "$workDir" ]] || mkdir -p "$workDir" 305 | fileManifest="${workDir}/manifest.list" 306 | logFile="${workDir}/createrepo.log" 307 | rm -f -- "$logFile" 308 | 309 | # Detect modularity 310 | localModules=$(ls ${inputDir}/${subpath}/${moduleName}* 2>/dev/null | awk NR==1) 311 | remoteModules=$(ls ${mirror}/${subpath}/${moduleName}* 2>/dev/null | awk NR==1) 312 | check_modular 313 | 314 | # Update RPM metadata 315 | rpm_metadata "$mirror" "$inputDir" "$subpath" 316 | 317 | ### END ### 318 | -------------------------------------------------------------------------------- /repo-debian.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC2068,SC2164,SC2086,SC2155,SC2207 3 | # Copyright 2021, NVIDIA Corporation 4 | # SPDX-License-Identifier: MIT 5 | 6 | publicKey="3bf863cc" # set this to shortname for GPG keypair 7 | 8 | 9 | err() { echo "ERROR: $*"; exit 1; } 10 | 11 | usage() { 12 | echo "USAGE: $0 [workdir] [gpgkey]" 13 | echo 14 | echo " PARAMETERS:" 15 | echo -e " --input=\t overlay with changes" 16 | echo -e " --mirror=\t source of truth" 17 | echo -e " --repo=\t \$distro/\$arch to traverse" 18 | echo 19 | echo " OPTIONAL:" 20 | echo -e " --nocache\t\t rebuild metadata" 21 | echo -e " --gpgkey=\t shortname for GPG signing keypair" 22 | echo -e " --workdir=\t scratch area for temp files" 23 | echo 24 | [[ -n $1 ]] && err "$*" 25 | [[ -z $1 ]] && exit 0 26 | } 27 | 28 | compare_file() { 29 | local file1=$(md5sum "$mirror/$2/$3" | awk '{print $1}') 30 | local file2=$(md5sum "$1/$2/$3" | awk '{print $1}') 31 | echo " -> [file1] $mirror/$2/$3: $file1" 32 | echo " -> [file2] $1/$2/$3: $file2" 33 | if [[ "$file1" == "$file2" ]]; then 34 | echo ":: files are identical" 35 | return 0 36 | else 37 | echo ":: files are different" 38 | return 1 39 | fi 40 | } 41 | 42 | get_checksum() { 43 | local dir="$1" 44 | md5sum "$dir"/* | sed 's|\/| |g' | awk '{print $1,$NF}' | sort -k2 45 | } 46 | 47 | deb_md5sum() { 48 | local parent="$1" 49 | local subpath="$2" 50 | 51 | debs=$(find "$parent/$subpath" -mindepth 1 -maxdepth 1 -type f \( -not -name "*.deb" \) 2>/dev/null | sort) 52 | if [[ -n "$debs" ]]; then 53 | echo "==> $parent/$subpath" 54 | md5sum $debs | sed 's|\/| |g' | awk '{print $1,$NF}' | sort -k2 55 | fi 56 | } 57 | 58 | compare_debian_md5sum() { 59 | [[ -d "$1" ]] || err "USAGE: compare_debian_md5sum() [dir2] [subpath]" 60 | [[ -d "$2" ]] || err "USAGE: compare_debian_md5sum() [dir1] [subpath]" 61 | [[ -n "$3" ]] || err "USAGE: compare_debian_md5sum() [dir1] [dir2] " 62 | 63 | file1=$(deb_md5sum "$1" "$3") 64 | file2=$(deb_md5sum "$2" "$3") 65 | 66 | #echo "$file1" 67 | #echo "---" 68 | #echo "$file2" 69 | echo "---------" 70 | 71 | two_way=$(comm -1 -3 <(echo "$file1" | sort) <(echo "$file2" | sort) | grep -v "^==>" | sort -k2) 72 | echo "$two_way" 73 | 74 | for line in $(echo "$two_way" | awk '{print $2}'); do 75 | echo "${subpath}/${line}" >> "$fileManifest" 76 | done 77 | 78 | diff_count=$(echo "$two_way" | wc -l) 79 | [[ $diff_count -gt 1 ]] || err "metadata unchanged" 80 | echo ":: $diff_count metadata file(s) added or modified" 81 | } 82 | 83 | deb_pkg_stale() { 84 | local donor_file=$(echo "$1" | sed -e 's|$|\.stale|' -e 's|\.old||') 85 | local stale_count=$(echo "$old_packages" | wc -l) 86 | [[ $stale_count -gt 0 ]] || return 87 | echo ">>> deb_pkg_stale($stale_count)" 88 | [[ -f "$1" ]] && 89 | rsync -a "$1" "$donor_file" 90 | 91 | for pkg in $old_packages; do 92 | context=$(grep -nE -e "^$" -e "^Filename:" "$donor_file" 2>/dev/null | grep -C1 -m1 -E "Filename:( |.*/)${pkg}$") 93 | range=($(echo "$context" | grep -v -e "$pkg" -e "^\-" | sed 's|:||g' | paste -d, - -)) 94 | for block_range in ${range[@]}; do 95 | length=$(wc -l "$donor_file" 2>/dev/null | awk '{print $1}') 96 | block_new=$(echo "$block_range" | awk -F "," '{print $1}') 97 | [[ "$block_new" =~ ^[0-9]+$ ]] || continue 98 | block_end=$(echo "$block_range" | awk -F "," '{print $2}') 99 | [[ "$block_end" =~ ^[0-9]+$ ]] || continue 100 | 101 | #tail -n +$block_new "$donor_file" | head -n $((block_end-block_new)) 102 | 103 | head -n "$block_new" "$donor_file" > "${donor_file}.head" 104 | tail -n $((length-block_end)) "$donor_file" > "${donor_file}.tail" 105 | cat "${donor_file}.head" "${donor_file}.tail" > "${donor_file}" 106 | echo ":: removed $pkg" 107 | rm -f "${donor_file}.head" "${donor_file}.tail" 108 | done 109 | done 110 | echo 111 | } 112 | 113 | find_tag() { 114 | local index=0 115 | for key in ${pkg_tags[@]}; do 116 | if [[ "$key" == "null" ]]; then 117 | pkg_tags[$index]="null" 118 | elif [[ "$key" == "$1" ]] || [[ -z "$1" ]]; then 119 | value=$(dpkg-deb --field "$filename" "$key") 120 | echo "$key: $value" 121 | pkg_tags[$index]="null" 122 | fi 123 | index=$((index+1)) 124 | done 125 | } 126 | 127 | deb_pkg_info() { 128 | # 129 | # Function replaces tools like apt-ftparchive and dpkg-scanpackages 130 | # 131 | 132 | [[ -f "$1" ]] || err "deb_pkg_info() file $1 not found" 133 | 134 | # Calculate some values 135 | pkg_size=$(du -L -b "$1" 2>/dev/null | awk '{print $1}') 136 | pkg_md5=$(md5sum "$1" 2>/dev/null | awk '{print $1}') 137 | pkg_sha1=$(sha1sum "$1" 2>/dev/null | awk '{print $1}') 138 | pkg_sha256=$(sha256sum "$1" 2>/dev/null | awk '{print $1}') 139 | pkg_sha512=$(sha512sum "$1" 2>/dev/null | awk '{print $1}') 140 | 141 | # Order matters 142 | filename="$1" 143 | pkg_tags=($(dpkg --info "$filename" | sed 's/^ //' | grep -E "^[A-Za-z-]+:" | awk -F ":" '{print $1}')) 144 | find_tag "Package" 145 | find_tag "Version" 146 | find_tag "Architecture" 147 | find_tag "Multi-Arch" 148 | find_tag "Priority" 149 | find_tag "Section" 150 | find_tag "Source" 151 | find_tag "Origin" 152 | find_tag "Maintainer" 153 | find_tag "Original-Maintainer" 154 | find_tag "Bugs" 155 | find_tag "Installed-Size" 156 | find_tag "Provides" 157 | find_tag "Depends" 158 | find_tag "Recommends" 159 | find_tag "Suggests" 160 | find_tag "Conflicts" 161 | find_tag "Breaks" 162 | find_tag "Replaces" 163 | 164 | # Append calculated values 165 | echo "Filename: ./$1" 166 | echo "Size: $pkg_size" 167 | echo "MD5sum: $pkg_md5" 168 | echo "SHA1: $pkg_sha1" 169 | echo "SHA256: $pkg_sha256" 170 | echo "SHA512: $pkg_sha512" 171 | 172 | # Append package description 173 | find_tag "Homepage" 174 | find_tag "Description" 175 | 176 | # Anything leftover 177 | find_tag 178 | } 179 | 180 | deb_metadata() { 181 | local donor="$1" 182 | local parent="$2" 183 | local subpath="$3" 184 | local oldPWD="$PWD" 185 | 186 | if [[ -d "${donor}/${subpath}" ]] && [[ -z "$nocache" ]]; then 187 | echo ":: Get bytes from donor Packages.gz" 188 | # Get bytes from donor Packages.gz 189 | cd "${donor}/${subpath}" || err "unable to cd to $donor / $subpath" 190 | donorManifest=$(gunzip -c Packages.gz 2>/dev/null) 191 | bytes1=$(echo "$donorManifest" | grep -e "^Filename:" -e "^Size:" | awk '{print $NF}') 192 | bytes1=$(echo "$bytes1" | sed 's|\.\/| |' | paste -d " " - - | awk '{print $2, $1}' | column -t | sort) 193 | 194 | cd "${parent}/${subpath}" || err "unable to cd to $parent / $subpath" 195 | echo -n "..." 196 | 197 | # Calculate bytes from local DEB packages 198 | bytes2=$(du -L -b -- *.deb | column -t | sort) 199 | echo -n "..." 200 | 201 | # Skip unmodified packages 202 | byte_compare=$(comm -1 -3 <(echo "$bytes1" | sort) <(echo "$bytes2")) 203 | deb_packages=$(echo "$byte_compare" | awk '{print $NF}' | sort) 204 | 205 | # Track modified packages 206 | stale_crumbs=$(comm -2 -3 <(echo "$bytes1" | sort) <(echo "$bytes2")) 207 | old_packages=$(echo "$stale_crumbs" | awk '{print $NF}' | sort) 208 | echo "..." 209 | else 210 | echo ":: From scratch" 211 | cd "${parent}/${subpath}" || err "unable to cd to $parent / $subpath" 212 | deb_packages=$(find . -maxdepth 1 -name "*.deb" -exec stat -c "%y %n" {} + 2>/dev/null | awk '{print $1,$NF}' | sort | awk '{print $NF}' | sed 's|\.\/| |') 213 | fi 214 | 215 | 216 | pkg_count=$(echo "$deb_packages" | grep -v ^$ | wc -l) 217 | [[ $pkg_count -gt 0 ]] || echo "WARNING: no new packages - intentionally empty?" 218 | echo ">>> deb_pkg_info($pkg_count)" 219 | cd "${inputDir}/${subpath}" || err "unable to cd to $inputDir / $subpath" 220 | 221 | # 222 | # Manually process new or modified Debian packages 223 | # 224 | PackagesNew="Packages.new" 225 | rm -f "$PackagesNew" 226 | touch "$PackagesNew" 227 | for pkg in $deb_packages; do 228 | deb_pkg_info "$pkg" >> "$PackagesNew" 229 | echo >> "$PackagesNew" 230 | echo "$pkg" 231 | echo ${subpath}/${pkg} >> "$fileManifest" 232 | done 233 | echo 234 | 235 | # Append and rename 236 | PackagesOld="Packages.old" 237 | PackagesFix="Packages.stale" 238 | if [[ -n "$donorManifest" ]]; then 239 | echo "$donorManifest" > "$PackagesOld" 240 | echo >> "$PackagesOld" 241 | 242 | deb_pkg_stale "$PackagesOld" 243 | [[ -f "$PackagesFix" ]] && PackagesOld="$PackagesFix" 244 | 245 | echo "[Merge] :: cat $PackagesOld $PackagesNew > Packages" 246 | cat "$PackagesOld" "$PackagesNew" > Packages || err "scratch image too small" 247 | else 248 | echo "[New] :: mv -v $PackagesNew Packages" 249 | mv -v "$PackagesNew" Packages 250 | fi 251 | 252 | [[ -f "Packages" ]] || err "Packages file not found" 253 | 254 | # Compress manifest 255 | gzip -c -9 -f Packages > Packages.gz 256 | echo ":: Packages.gz" 257 | [[ -f "Packages.gz" ]] || err "Packages.gz file not found" 258 | 259 | # Calculate hashes 260 | txt_bytes=$(wc --bytes Packages | awk '{print $1}') 261 | txt_md5=$(md5sum Packages | awk '{print $1}') 262 | txt_sha1=$(sha1sum Packages | awk '{print $1}') 263 | txt_sha256=$(sha256sum Packages | awk '{print $1}') 264 | 265 | gz_bytes=$(wc --bytes Packages.gz | awk '{print $1}') 266 | gz_md5=$(md5sum Packages.gz | awk '{print $1}') 267 | gz_sha1=$(sha1sum Packages.gz | awk '{print $1}') 268 | gz_sha256=$(sha256sum Packages.gz | awk '{print $1}') 269 | 270 | # Build checksum file 271 | Release="Release.new" 272 | pkg_arch=$(basename "$subpath") 273 | pkg_date=$(date -R -u) 274 | { 275 | echo "Origin: NVIDIA" 276 | echo "Label: NVIDIA CUDA" 277 | echo "Architecture: ${pkg_arch}" 278 | echo "Date: ${pkg_date}" 279 | echo "MD5Sum:" 280 | printf " %s %48d %s\n" $txt_md5 $txt_bytes Packages 281 | printf " %s %48d %s\n" $gz_md5 $gz_bytes Packages.gz 282 | echo "SHA1:" 283 | printf " %s %40d %s\n" $txt_sha1 $txt_bytes Packages 284 | printf " %s %40d %s\n" $gz_sha1 $gz_bytes Packages.gz 285 | echo "SHA256:" 286 | printf " %s %16d %s\n" $txt_sha256 $txt_bytes Packages 287 | printf " %s %16d %s\n" $gz_sha256 $gz_bytes Packages.gz 288 | 289 | # FIXME prevent hash mismatch error 290 | echo "Acquire-By-Hash: no" 291 | } > "$Release" 292 | 293 | # Rename 294 | mv -v "$Release" Release 295 | [[ -f "Release" ]] || err "Release file not found" 296 | echo ":: Release" 297 | cat Release 298 | echo 299 | 300 | echo "==> Sanity check for Release" 301 | if [[ -f "$mirror/$subpath/Release" ]]; then 302 | compare_file "$parent" "$subpath" "Release" && err "expected new metadata" 303 | else 304 | echo " :: Old repo not found" 305 | fi 306 | echo 307 | 308 | # Sign checksum file with key 309 | if [[ $gpgkeyName == "UNSIGNED" ]]; then 310 | echo "==> Skipping signing (use external signing server)" 311 | else 312 | echo ">>> gpg -u ${gpgkeyName} --yes --armor --detach-sign --personal-digest-preferences SHA512 --output Release.gpg Release" 313 | gpg -u ${gpgkeyName} --yes --armor --detach-sign --personal-digest-preferences SHA512 --output Release.gpg Release || err "gpg failed to detach signature" 314 | [[ -f "Release.gpg" ]] || err "Release.gpg file not found" 315 | echo ":: Release.gpg" 316 | gpg -u ${gpgkeyName} --yes --clearsign --personal-digest-preferences SHA256 --output InRelease Release || err "InRelease failed" 317 | [[ -f "InRelease" ]] || err "InRelease file not found" 318 | echo ":: InRelease" 319 | fi 320 | 321 | cd "$oldPWD" >/dev/null 322 | echo 323 | 324 | if [[ -z "$nocache" ]]; then 325 | compare_debian_md5sum "$mirror" "$inputDir" "$subpath" 326 | echo 327 | fi 328 | } 329 | 330 | 331 | # Options 332 | while [[ $1 =~ ^-- ]]; do 333 | # Full rebuild of metadata 334 | if [[ $1 =~ ^--nocache$ ]] || [[ $1 =~ ^--no-cache$ ]]; then 335 | nocache=1 336 | # Repository relative path 337 | elif [[ $1 =~ "repo=" ]]; then 338 | subpath=$(echo "$1" | awk -F "=" '{print $2}') 339 | elif [[ $1 =~ ^--repo$ ]]; then 340 | shift; subpath="$1" 341 | # Repository architecture 342 | elif [[ $1 =~ "arch=" ]]; then 343 | arch=$(echo "$1" | awk -F "=" '{print $2}') 344 | elif [[ $1 =~ ^--arch$ ]]; then 345 | shift; arch="$1" 346 | # Repository distro name 347 | elif [[ $1 =~ "distro=" ]]; then 348 | distro=$(echo "$1" | awk -F "=" '{print $2}') 349 | elif [[ $1 =~ ^--distro$ ]]; then 350 | shift; distro="$1" 351 | # Scratch directory 352 | elif [[ $1 =~ "workdir=" ]]; then 353 | workDir=$(echo "$1" | awk -F "=" '{print $2}') 354 | elif [[ $1 =~ ^--workdir$ ]]; then 355 | shift; workDir="$1" 356 | # Source of truth 357 | elif [[ $1 =~ "mirror=" ]]; then 358 | mirror=$(echo "$1" | awk -F "=" '{print $2}') 359 | elif [[ $1 =~ ^--mirror$ ]]; then 360 | shift; mirror="$1" 361 | # Release candidate 362 | elif [[ $1 =~ "input=" ]]; then 363 | inputDir=$(echo "$1" | awk -F "=" '{print $2}') 364 | elif [[ $1 =~ ^--input$ ]]; then 365 | shift; inputDir="$1" 366 | # Signing key name 367 | elif [[ $1 =~ "gpg=" ]] || [[ $1 =~ "gpgkey=" ]]; then 368 | gpgkeyName=$(echo "$1" | awk -F "=" '{print $2}') 369 | elif [[ $1 =~ ^--gpg$ ]] || [[ $1 =~ ^--gpgkey$ ]]; then 370 | shift; gpgkeyName="$1" 371 | elif [[ $1 =~ ^--help$ ]]; then 372 | usage 373 | fi 374 | shift 375 | done 376 | 377 | 378 | [[ -d "$inputDir" ]] || usage "Must specify --input directory (read-write)" 379 | [[ ! -d "$mirror" ]] && [[ -z $nocache ]] && usage "Must specify --mirror directory (read-only)" 380 | [[ -d "$mirror" ]] && [[ -z "$subpath" ]] && usage "Must specify --repo relative subdirectory (\$distro/\$arch)" 381 | 382 | # Set default signing key 383 | [[ -n "$gpgkeyName" ]] || gpgkeyName="$publicKey" 384 | 385 | # Temp files 386 | [[ -n "$workDir" ]] || workDir=$(mktemp -d) 387 | [[ -d "$workDir" ]] || mkdir -p "$workDir" 388 | fileManifest="${workDir}/manifest.list" 389 | 390 | # Update Debian metadata 391 | deb_metadata "$mirror" "$inputDir" "$subpath" 392 | 393 | ### END ### 394 | -------------------------------------------------------------------------------- /repo-overlay.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC2001,SC2012,SC2048,SC2068,SC2076,SC2086,SC2128,SC2155,SC2199 3 | # Copyright 2021, NVIDIA Corporation 4 | # SPDX-License-Identifier: MIT 5 | 6 | current=$(readlink -e "$(dirname ${BASH_SOURCE[0]})") 7 | debianMetadata="${current}/repo-debian.sh" 8 | rpmMetadata="${current}/repo-rpm.sh" 9 | 10 | outputDir="$PWD/new" 11 | tempDir="$PWD" 12 | imgSize="2048" # 2 GiB 13 | 14 | uidmapping="squash_to_uid=$(id -u),squash_to_gid=$(id -g)" 15 | FUSEOPTS+="$uidmapping" 16 | 17 | clean_up() { 18 | cd / 19 | [[ -d "$mountDir" ]] && [[ -n "$rootless" ]] && fusermount -u "$mountDir" 20 | [[ -d "$mountDir" ]] && [[ -z "$rootless" ]] && sudo umount "$mountDir" 21 | [[ -d "$mountDir" ]] && rmdir "$mountDir" 22 | [[ -d "$mountDir" ]] && [[ -z "$rootless" ]] && sudo umount -l "$mountDir" 23 | [[ -d "$scratch" ]] && [[ -z "$rootless" ]] && sudo umount "$scratch" 24 | [[ -f "$onetimeFS" ]] && rm "$onetimeFS" 25 | [[ -d "$mountDir" ]] && rmdir "$mountDir" 26 | [[ -d "$scratch" ]] && rm -rf "$scratch" 27 | [[ -d "$tempDir/tmp" ]] && rmdir "$tempDir/tmp" 28 | } 29 | 30 | trap ctrl_c INT 31 | ctrl_c() { 32 | echo "==> Mischief managed" 33 | clean_up 34 | exit 1 35 | } 36 | 37 | err() { 38 | echo "ERROR: $*" 39 | clean_up 40 | exit 1 41 | } 42 | 43 | usage() { 44 | echo "USAGE: $0 [options] { [dir] ... }" 45 | echo 46 | echo " PARAMETERS:" 47 | echo -e " --mirror=\t source of truth public snapshot\t\t $mirror" 48 | echo -e " \t\t\t one or more input directories to overlay\t $ARGS" 49 | echo 50 | echo " OPTIONS:" 51 | echo -e " --filter=\t limit to select distro/arch repo(s)\t\t\t ${filter[*]}" 52 | echo -e " --output=\t the save location\t\t\t\t $outputDir" 53 | echo -e " --tempdir=\t scratch area for temporary files\t\t $tempDir" 54 | echo -e " --size=\t image file size for overlay\t\t\t (default: $imgSize)" 55 | echo -e " --keep-new\t\t save the new files\t\t\t\t $savepkgs" 56 | echo -e " --no-cache\t\t do not use mirror as base\t\t\t\t $nocache" 57 | echo -e " --no-sign\t\t do not use GPG sign metadata (use external sign server)\t\t\t\t $nosign" 58 | echo -e " --no-sparse\t\t do not sparsely allocate image size" 59 | echo -e " --debug\t\t verbose output\t\t $debug" 60 | echo -e " --clean\t\t un-mount stale overlayFS mountpoints\t\t $clean" 61 | echo 62 | 63 | if [[ -n $1 ]]; then 64 | echo "ERROR: $*" 65 | exit 1 66 | else 67 | exit 0 68 | fi 69 | } 70 | 71 | run_cmd() { 72 | echo 73 | echo ">>> $*" | fold -s 74 | time eval "$*" 75 | } 76 | 77 | run_rsync() { 78 | run_cmd rsync -av $* 79 | } 80 | 81 | preflight_checks() { 82 | # Minimum requirements 83 | kernelMajor=$(uname -r 2>/dev/null | awk -F '.' '{print $1}') 84 | pythonMajor=$(type -p python3 2>/dev/null) 85 | createRepo=$(type -p createrepo_c createrepo 2>/dev/null | awk NR==1) 86 | [[ $kernelMajor -gt 3 ]] || err "Kernel must be 4.x or newer for overlayFS" 87 | [[ $pythonMajor =~ "3" ]] || err "Python 3.x required for modularity" 88 | [[ -n $createRepo ]] || err "Missing depends for RPM repos" 89 | 90 | # FIXME 91 | userGroup=$(ls --color=none -ld $PWD 2>/dev/null | awk '{print $3":"$4}') 92 | 93 | # Cleanup previous runs 94 | rm -f -- "$fileManifest" 95 | clean_up 96 | 97 | # Create scratch area 98 | mkdir -p "$tempDir/tmp" 99 | cd "$tempDir/tmp" >/dev/null || err "unable to cd to $tempDir/tmp" 100 | 101 | # Pass-through 102 | if [[ -n $nocache ]]; then 103 | passthrough+=" --nocache " 104 | fi 105 | 106 | # Disable GPG signing 107 | if [[ -n $nosign ]]; then 108 | passthrough+=" --gpgkey=UNSIGNED " 109 | fi 110 | } 111 | 112 | package_metadata() { 113 | echo ":: Generate repo metadata" 114 | repos=$(find ${active[@]} -mindepth 2 -maxdepth 2 -type d 2>/dev/null | rev | sed -e 's|/|\t|' -e 's|/|\t|' | rev | sort -k2,3 -r | uniq -f1 | sed 's|\t|/|g') 115 | echo $repos 116 | for path in $repos; do 117 | unset subpath moreArgs unknownDistro skipNext 118 | subpath=$(echo $path | awk -F "/" '{print $(NF-1)"/"$(NF)}') 119 | rpmFormat=$(ls $path/*.rpm 2>/dev/null | awk NR==1) 120 | debFormat=$(ls $path/*.deb 2>/dev/null | awk NR==1) 121 | echo 122 | 123 | if [[ -n "$filter" ]]; then 124 | if [[ ! " ${filter[@]} " =~ " $subpath " ]]; then 125 | skipNext=1 126 | fi 127 | fi 128 | 129 | subdist=$(dirname "$subpath" 2>/dev/null | grep -v "^\.$") 130 | subarch=$(basename "$subpath" 2>/dev/null | grep -v "^\.$") 131 | if [[ -z "$subdist" ]] || [[ -z "$subarch" ]]; then 132 | unknownDistro=1 133 | elif [[ "$subdist" == "$subarch" ]]; then 134 | unknownDistro=1 135 | else 136 | moreArgs=" --distro=$subdist --arch=$subarch " 137 | fi 138 | 139 | if [[ -n "$skipNext" ]]; then 140 | echo "==> skipping repo: $subpath" 141 | elif [[ -n "$rpmFormat" ]]; then 142 | unset rpmFormat 143 | echo "==> rpm_metadata $passthrough $moreArgs --input $mountDir --mirror $mirror --repo $subpath" 144 | time $rpmMetadata $passthrough $moreArgs --input "$mountDir" --mirror "$mirror" --repo "$subpath" || err "RPM metadata failed" 145 | echo 146 | elif [[ -n "$debFormat" ]]; then 147 | unset debFormat 148 | echo "==> deb_metadata $passthrough $moreArgs --input $mountDir --mirror $mirror --repo $subpath" 149 | time $debianMetadata $passthrough $moreArgs --input "$mountDir" --mirror "$mirror" --repo "$subpath" || err "Debian metadata failed" 150 | echo 151 | else 152 | echo "==> skipping unimplemented format: $subpath $moreArgs" 153 | fi 154 | done 155 | echo 156 | } 157 | 158 | min_tempsize() { 159 | echo "==> Calculating scratch size" 160 | local depthTwo=$(find $@ -mindepth 2 -maxdepth 2 -type d 2>/dev/null | sort -r) 161 | unset sumLocal sumRemote 162 | for repoPath in $depthTwo; do 163 | local subdir=$(echo $repoPath | awk -F "/" '{print $(NF-1)"/"$(NF)}') 164 | echo -n "." 165 | local localSize=$(du -sc -BM ${repoPath}/repodata ${repoPath}/Packages* 2>/dev/null | tail -n 1 | awk -F "M" '{print $1}') 166 | local remoteSize=$(du -sc -BM ${mirror}/${subdir}/repodata ${mirror}/${subdir}/Packages* 2>/dev/null | tail -n 1 | awk -F "M" '{print $1}') 167 | sumLocal=$((sumLocal + localSize)) 168 | sumRemote=$((sumRemote + remoteSize)) 169 | done 170 | 171 | repoSum=$((sumLocal + sumRemote)) 172 | echo 173 | 174 | # Powers of 2 175 | for n in $(seq 1 13); do 176 | powerTwo=$((2**n)) 177 | if [[ $repoSum -le $powerTwo ]]; then 178 | break 179 | fi 180 | done 181 | 182 | # Sanity check 183 | if [[ $repoSum -gt $powerTwo ]]; then 184 | err "Power of 2 overflow detected (${repoSum}M > ${powerTwo}M, specify custom --size" 185 | fi 186 | 187 | echo ":: Optimal scratch size: ${repoSum}M -> ${powerTwo}M" 188 | imgSize=$((powerTwo + repoSum)) 189 | } 190 | 191 | mount_tempfs() { 192 | if [[ -z $nosparse ]] && [[ -z $customSize ]]; then 193 | # 16GB max, only allocate as much as needed 194 | imgSize=$((imgSize * 8)) 195 | fi 196 | 197 | echo ":: Create scratch filesystem: $scratch [$imgSize MB]" 198 | 199 | if [[ -n $nosparse ]]; then 200 | echo ">>> dd if=/dev/zero of=$onetimeFS bs=1M count=${imgSize}" 201 | dd if=/dev/zero of="$onetimeFS" bs=1M count=${imgSize} || err "dd ${onetimeFS} ${imgSize}MB" 202 | else 203 | echo ">>> truncate -s ${imgSize} $onetimeFS" 204 | truncate -s ${imgSize}M "$onetimeFS" 205 | fi 206 | 207 | du -sch "$onetimeFS" 208 | mkfs -t ext4 -F "$onetimeFS" || err "mkfs.ext4" 209 | 210 | mkdir -p $scratch || err "mkdir -p $scratch" 211 | echo ">>> sudo mount -t ext4 $onetimeFS $scratch" 212 | sudo mount -t ext4 "$onetimeFS" "$scratch" || err "mount -t ext4 $onetimeFS $scratch" 213 | 214 | echo "userGroup == $userGroup" 215 | sudo chown -R $userGroup "$scratch" || err "chown $userGroup scratch" 216 | ls -l "$scratch" 217 | echo 218 | } 219 | 220 | mount_overlay() { 221 | echo ":: Overlay filesystems as layers: $mountDir" 222 | mkdir -p $mountDir || err "mkdir -p $mountDir" 223 | mkdir -p $scratch/{upper,workdir} || err "mkdir -p $scratch/{upper,workdir}" 224 | [[ -d "$scratch/upper" ]] || err "directory $scratch/upper does not exist" 225 | [[ -d "$scratch/workdir" ]] || err "directory $scratch/workdir does not exist" 226 | [[ -d "$mountDir" ]] || err "directory $mountDir does not exist" 227 | 228 | if [[ $rootless -eq 1 ]]; then 229 | [[ -n $FUSEOPTS ]] && FUSEOPTS="${FUSEOPTS}," 230 | echo ">>> fuse-overlayfs -o ${FUSEOPTS}lowerdir=${layers},upperdir=${scratch}/upper,workdir=${scratch}/workdir none $mountDir" 231 | fuse-overlayfs -o ${FUSEOPTS}lowerdir=${layers},upperdir=${scratch}/upper,workdir=${scratch}/workdir none $mountDir || err "mount -t overlay" 232 | else 233 | echo ">>> sudo mount -t overlay -o lowerdir=${layers},upperdir=${scratch}/upper,workdir=${scratch}/workdir none $mountDir" 234 | sudo mount -t overlay -o lowerdir=${layers},upperdir=${scratch}/upper,workdir=${scratch}/workdir none $mountDir || err "mount -t overlay" 235 | fi 236 | echo 237 | } 238 | 239 | save_new_metadata() { 240 | echo ":: Saving metadata to $outputDir" 241 | # Metadata 242 | run_rsync "${scratch}/upper"/ "$outputDir" 243 | echo 244 | } 245 | 246 | save_new_packages() { 247 | echo ":: Saving packages to $outputDir" 248 | # Packages 249 | repos=$(find ${active[@]} -mindepth 2 -maxdepth 2 -type d 2>/dev/null | sort) 250 | for path in $repos; do 251 | subpath=$(echo $path | awk -F "/" '{print $(NF-1)"/"$(NF)}') 252 | if [[ -n "$filter" ]]; then 253 | if [[ ! " ${filter[@]} " =~ " $subpath " ]]; then 254 | continue 255 | fi 256 | fi 257 | 258 | mkdir -p "$outputDir/${subpath}"/ 259 | run_rsync --include="*.deb" --include="*.rpm" --include="*.repo" --include="*.pin" --include="*.pub" --include="*.json" --exclude="*" "${path}"/ "$outputDir/${subpath}"/ 260 | 261 | if [[ -f "${path}/precompiled/index.html" ]]; then 262 | run_rsync --include="*.html" --exclude="*" "${path}/precompiled"/ "$outputDir/${subpath}/precompiled"/ 263 | fi 264 | done 265 | echo 266 | } 267 | 268 | 269 | # Overrides 270 | while [[ $1 =~ ^-- ]]; do 271 | # Filter repos 272 | if [[ "$1" =~ "--filter=" ]]; then 273 | include=$(echo "$1" | awk -F '=' '{print $2}') 274 | filter+=("$include") 275 | elif [[ "$1" =~ ^--filter$ ]]; then 276 | shift; filter+=("$1") 277 | # Output directory 278 | elif [[ "$1" =~ "--output=" ]]; then 279 | outputDir=$(echo "$1" | awk -F '=' '{print $2}') 280 | elif [[ "$1" =~ ^--output$ ]]; then 281 | shift; outputDir="$1" 282 | # Scratch directory 283 | elif [[ $1 =~ "tempdir=" ]] || [[ $1 =~ "workdir=" ]]; then 284 | tempDir=$(echo "$1" | awk -F "=" '{print $2}') 285 | elif [[ $1 =~ ^--tempdir$ ]] || [[ $1 =~ ^--workdir$ ]]; then 286 | shift; tempDir="$1" 287 | # Source of truth 288 | elif [[ $1 =~ "mirror=" ]]; then 289 | mirror=$(readlink -m $(echo "$1" | awk -F "=" '{print $2}')) 290 | elif [[ $1 =~ ^--mirror$ ]]; then 291 | shift; mirror=$(readlink -m "$1") 292 | # Specify image size 293 | elif [[ "$1" =~ "--size=" ]]; then 294 | customSize=$(echo "$1" | awk -F '=' '{print $2}') 295 | imgSize="$customSize" 296 | elif [[ "$1" =~ --size$ ]]; then 297 | shift; customSize="$1" 298 | imgSize="$customSize" 299 | # Save new packages 300 | elif [[ "$1" == "--keep-new" ]]; then 301 | savepkgs=1 302 | # Do not use source of truth 303 | elif [[ "$1" == "--no-cache" ]] || [[ "$1" == "--nocache" ]]; then 304 | nocache=1 305 | # Disable metadata signing 306 | elif [[ "$1" == "--no-sign" ]] || [[ "$1" == "--nosign" ]]; then 307 | nosign=1 308 | # Sparsely allocate image size 309 | elif [[ "$1" == "--no-sparse" ]] || [[ "$1" == "--nosparse" ]]; then 310 | nosparse=1 311 | # Clean overlayFS mount 312 | elif [[ "$1" == "--clean" ]]; then 313 | clean=1 314 | # Verbose 315 | elif [[ "$1" == "--debug" ]]; then 316 | debug=1 317 | # Use FUSE implementation of overlayFS (does not require root permissions!) 318 | elif [[ "$1" == "--fuse" ]] || [[ -n $ROOTLESS ]] || [[ -n $FUSEOVERLAY ]]; then 319 | rootless=1 320 | # Usage 321 | elif [[ "$1" == "--help" ]]; then 322 | usage 323 | else 324 | echo -e "\nERROR: unknown parameter: $1\n" 325 | usage 326 | fi 327 | shift 328 | done 329 | 330 | 331 | ARGS=$(echo "$@" | sed 's| |\n\t\t\t\t\t\t\t\t\t |g') 332 | fileManifest="${tempDir}/manifest.list" 333 | onetimeFS="${tempDir}/tmp/upper.img" 334 | scratch="${tempDir}/tmp/scratch" 335 | mountDir="${tempDir}/tmp/overlay" 336 | 337 | if [[ -n $debug ]]; then 338 | echo "temp: $tempDir" 339 | echo "mirror: $mirror" 340 | echo "input dirs: $@" 341 | echo "output: $outputDir" 342 | fi 343 | 344 | if [[ -n $clean ]]; then 345 | clean_up 346 | exit 0 347 | fi 348 | 349 | if [[ -n $nocache ]]; then 350 | mkdir -p "$tempDir/empty" 351 | mirror="$tempDir/empty" 352 | fi 353 | 354 | # Prepare overlayFS parameters 355 | unset layers 356 | for i in $@; do 357 | dir=$(readlink -m "$i") 358 | [[ -d "$dir" ]] && 359 | layers+="${dir}:" && 360 | active+=("$dir") 361 | shift 362 | done 363 | layers+="$mirror" 364 | 365 | # Sanity checks 366 | [[ -d $mirror ]] || usage "Must specify --mirror path to public snapshot [$mirror]" 367 | [[ -d $dir ]] || usage "Must specify at least one directory to overlay [$dir]" 368 | preflight_checks "$1" 369 | 370 | # Sanity 371 | if [[ -z $active ]]; then 372 | echo ":: No directories in overlay, bailing" 373 | exit 1 374 | fi 375 | 376 | if [[ -n "$nosparse" ]] && [[ -z "$customSize" ]]; then 377 | min_tempsize ${active[@]} 378 | fi 379 | 380 | # Create scratch filesystem 381 | if [[ -z $rootless ]]; then 382 | mount_tempfs 383 | else 384 | echo "==> scratch: $scratch" 385 | fi 386 | 387 | # Overlay filesystems as layers 388 | mount_overlay 389 | 390 | # Generate repo metadata 391 | package_metadata 392 | 393 | du -sch "$onetimeFS" 394 | 395 | # Save new packages to disk 396 | if [[ -n $savepkgs ]]; then 397 | save_new_packages 398 | fi 399 | 400 | # Save metadata to disk 401 | save_new_metadata 402 | 403 | # Remove temp files 404 | clean_up 405 | [[ -d "$tempDir/empty" ]] && 406 | rmdir "$tempDir/empty" 407 | 408 | true 409 | ### END ### 410 | -------------------------------------------------------------------------------- /SIGNATURES.md: -------------------------------------------------------------------------------- 1 | # signatures 2 | 3 | ## Overview 4 | 5 | How to verify RPM & Debian package and repo signatures 6 | 7 | ## Table of Contents 8 | 9 | - [Overview](#overview) 10 | - [Verify packages](#verify-packages) 11 | * [Debian packages](#debian-packages) 12 | - [Debian package method 1](#debian-package-method-1) 13 | - [Debian package method 2](#debian-package-method-2) 14 | * [RPM packages](#rpm-packages) 15 | - [RPM package method 1](#rpm-package-method-1) 16 | - [RPM package method 2](#rpm-package-method-2) 17 | - [Verify repository](#verify-repository) 18 | * [Debian repo](#debian-repo) 19 | - [Debian repo method 1](#debian-repo-method-1) 20 | - [Debian repo method 2](#debian-repo-method-2) 21 | - [Debian repo method 3](#debian-repo-method-3) 22 | * [RPM repo](#rpm-repo) 23 | - [RPM repo method 1](#rpm-repo-method-1) 24 | - [RPM repo method 2](#rpm-repo-method-2) 25 | 26 | 27 | ## Verify packages 28 | 29 | > NOTE: The recommended way to validate is to create a network repository and try installing the packages using the CLI apt-get/yum/dnf/zypper package manager. 30 | 31 | 32 | ### Debian packages 33 | 34 | Un-packing the `_gpgbuilder` file from the archive reveals a message and signature. This requires some processing to validate. 35 | 36 | ##### Example 1 37 | ```shell 38 | $ ar -p cuda-keyring_1.0-1_all.deb _gpgbuilder 39 | ``` 40 | 41 |
42 | Expand 43 | 44 | ```shell 45 | -----BEGIN PGP SIGNED MESSAGE----- 46 | Hash: SHA512 47 | 48 | Version: 4 49 | Signer: cudatools 50 | Date: Fri Apr 22 09:56:53 2022 51 | Role: builder 52 | Files: 53 | 3cf918272ffa5de195752d73f3da3e5e 7959c969e092f2a5a8604e2287807ac5b1b384ad 4 debian-binary 54 | 326ddb43903cf2a9ba2559039d24e4c7 3b8302d1606c40e6645dff296e6118dd407fe6a3 900 control.tar.xz 55 | 37991612ad00c6c8572666bbf5060e75 5498d47b1a177abdedc89ba2ec04e37d367951ae 1908 data.tar.xz 56 | -----BEGIN PGP SIGNATURE----- 57 | Version: GnuPG v2.0.22 (GNU/Linux) 58 | 59 | iQIcBAEBCgAGBQJiYnvlAAoJEKS0aZY7+GPMg+MP/RIO2ZOS5zHfmMvn6WC5z370 60 | CNchdGLHodvVHPgF75zw/dh7kcHWULfkV8WkuYMiJwhm0zGTOSx3TTxGiSI/5uEU 61 | Wq4QZQmxCnWmvDfxNGuSs/NoeA2ZHHyMAswZvuIu35fc9uR9aPz7T3dhVw1Usuv7 62 | ENKaHbt8NOOmh6osAGjrrDx1/LM9XmvjCvqxduDYFnq9yIJ/KCwxQLL8afzwLjym 63 | qobpbWqcvzWyavZkXwI4AMpTZY+myGRA3CGpGnxKEmokHJC0XvT3eJRecRCzk/Ir 64 | 9msMyJJdU9ntNDF2Aup6uTaY7ACYgEo9W2IBUK7Y/YnbOK4YdYXdrTzzq6IerfK1 65 | l/zCCdZ2951o6xPnZsRC3pb60n3RcpQehSkl5IVJXGA+IDFS50OtQKALuVVdCSf8 66 | wuz1gEFQjIUkY3/QRh+7hw9AnJSQF9grtSZElzndnIhE3JczndA1/vGni1gfgJUI 67 | c0NHXC+PUft+tNG/oahEE+NrXzUciyqlSeKGGniTvRoQDzlQfag9XcOkWJaSlJm7 68 | bJZ1QCCRQPDzZp/aL+H7K+w3kUQibdrwrqpialB0jsDCsAE56e+4ptPddBHisFBG 69 | FfkTpFNCAFFp+ylxUfyVaeGbDJ61YJSpLcICGTtbkQLCv08WOSWLzE2OdVnh94Pm 70 | kGTdUd+T6SdLNP0p7L5A 71 | =CaAY 72 | -----END PGP SIGNATURE----- 73 | ``` 74 | 75 |
76 | 77 | 78 | _Setup a test environment_ 79 | ```shell 80 | setup='DEBIAN_FRONTEND=noninteractive apt-get install -y binutils gnupg wget ca-certificates' 81 | docker run -it ubuntu:20.04 /bin/bash -c "apt-get update && $setup; bash" 82 | baseurl="https://developer.download.nvidia.com/compute/cuda/repos" 83 | wget $baseurl/ubuntu2004/x86_64/cuda-keyring_1.0-1_all.deb 84 | wget $baseurl/ubuntu2004/x86_64/3bf863cc.pub 85 | ``` 86 | 87 | #### Debian package method 1 88 | ```shell 89 | checksig_deb() { ar -p "$1" _gpgbuilder 2>&1 | gpg --openpgp --decrypt --no-auto-check-trustdb --batch --no-tty --status-fd 1 2>&1; } 90 | checksig_deb *.deb 91 | ``` 92 | 93 | ##### Example 2 94 | 95 | ```shell 96 | $ checksig_deb() { ar -p "$1" _gpgbuilder 2>&1 | gpg --openpgp --decrypt --no-auto-check-trustdb --batch --no-tty --status-fd 1 2>&1; } 97 | $ checksig_deb cuda-keyring_1.0-1_all.deb 98 | ``` 99 | 100 |
101 | Expand 102 | 103 | ```shell 104 | [GNUPG:] PLAINTEXT 74 0 105 | Version: 4 106 | Signer: cudatools 107 | Date: Fri Apr 22 09:56:53 2022 108 | Role: builder 109 | Files: 110 | 3cf918272ffa5de195752d73f3da3e5e 7959c969e092f2a5a8604e2287807ac5b1b384ad 4 debian-binary 111 | 326ddb43903cf2a9ba2559039d24e4c7 3b8302d1606c40e6645dff296e6118dd407fe6a3 900 control.tar.xz 112 | 37991612ad00c6c8572666bbf5060e75 5498d47b1a177abdedc89ba2ec04e37d367951ae 1908 data.tar.xz 113 | [GNUPG:] NEWSIG 114 | gpg: Signature made Fri Apr 22 09:56:53 2022 UTC 115 | gpg: using RSA key A4B469963BF863CC 116 | [GNUPG:] ERRSIG A4B469963BF863CC 1 10 01 1650621413 9 - 117 | ``` 118 | 119 | Notice that it cannot validate the signature if the public key is not found 120 | 121 | ```shell 122 | [GNUPG:] NO_PUBKEY A4B469963BF863CC 123 | gpg: Can't check signature: No public key 124 | ``` 125 | 126 | then import the GPG public key 127 | 128 | ```shell 129 | $ gpg --import 3bf863cc.pub 130 | $ checksig_deb cuda-keyring_1.0-1_all.deb 131 | ``` 132 | 133 | ```shell 134 | [GNUPG:] PLAINTEXT 74 0 135 | Version: 4 136 | Signer: cudatools 137 | Date: Fri Apr 22 09:56:53 2022 138 | Role: builder 139 | Files: 140 | 3cf918272ffa5de195752d73f3da3e5e 7959c969e092f2a5a8604e2287807ac5b1b384ad 4 debian-binary 141 | 326ddb43903cf2a9ba2559039d24e4c7 3b8302d1606c40e6645dff296e6118dd407fe6a3 900 control.tar.xz 142 | 37991612ad00c6c8572666bbf5060e75 5498d47b1a177abdedc89ba2ec04e37d367951ae 1908 data.tar.xz 143 | [GNUPG:] NEWSIG 144 | gpg: Signature made Fri Apr 22 09:56:53 2022 UTC 145 | gpg: using RSA key A4B469963BF863CC 146 | [GNUPG:] KEY_CONSIDERED EB693B3035CD5710E231E123A4B469963BF863CC 0 147 | [GNUPG:] SIG_ID Mx3zUme9SMeQ67oDvcbN449zOzQ 2022-04-22 1650621413 148 | [GNUPG:] KEY_CONSIDERED EB693B3035CD5710E231E123A4B469963BF863CC 0 149 | ``` 150 | 151 | ```shell 152 | [GNUPG:] GOODSIG A4B469963BF863CC cudatools 153 | gpg: Good signature from "cudatools " [unknown] 154 | [GNUPG:] VALIDSIG EB693B3035CD5710E231E123A4B469963BF863CC 2022-04-22 1650621413 0 4 0 1 10 01 EB693B3035CD5710E231E123A4B469963BF86> 155 | ``` 156 | 157 | ```shell 158 | [GNUPG:] TRUST_UNDEFINED 0 pgp 159 | gpg: WARNING: This key is not certified with a trusted signature! 160 | gpg: There is no indication that the signature belongs to the owner. 161 | Primary key fingerprint: EB69 3B30 35CD 5710 E231 E123 A4B4 6996 3BF8 63CC 162 | [GNUPG:] VERIFICATION_COMPLIANCE_MODE 23 163 | ``` 164 | 165 | notice it now says "VALIDSIG" instead of "NO_PUBKEY" 166 | 167 | ```shell 168 | $ gpg --delete-keys 3bf863cc 169 | ``` 170 | 171 |
172 | 173 | 174 | #### Debian package method 2 175 | ```shell 176 | gpgbuilder=$(ar -p *.deb _gpgbuilder) 177 | message=$(echo "$gpgbuilder" | sed -n '/-----BEGIN PGP SIGNATURE-----/q;p') 178 | detached=$(echo "$gpgbuilder" | sed -n '/-----BEGIN PGP SIGNATURE-----/,$p') 179 | gpg --verify <(echo "$detached") <(echo "$message") 180 | ``` 181 | 182 | ##### Example 3 183 | 184 |
185 | Expand 186 | 187 | ```shell 188 | $ gpgbuilder=$(ar -p cuda-keyring_1.0-1_all.deb _gpgbuilder) 189 | $ message=$(echo "$gpgbuilder" | sed -n '/-----BEGIN PGP SIGNATURE-----/q;p') 190 | $ detached=$(echo "$gpgbuilder" | sed -n '/-----BEGIN PGP SIGNATURE-----/,$p') 191 | ``` 192 | 193 | using process substitution 194 | 195 | ```shell 196 | $ gpg --verify <(echo "$detached") <(echo "$message") 197 | gpg: Signature made Fri Apr 22 09:56:53 2022 UTC 198 | gpg: using RSA key A4B469963BF863CC 199 | gpg: Can't check signature: No public key 200 | ``` 201 | 202 | then import the GPG public key 203 | 204 | ```shell 205 | $ gpg --import 3bf863cc.pub 206 | $ gpg --verify <(echo "$detached") <(echo "$message") 207 | gpg: Signature made Fri Apr 22 09:56:53 2022 UTC 208 | gpg: using RSA key A4B469963BF863CC 209 | gpg: BAD signature from "cudatools " [unknown] 210 | ``` 211 | 212 | notice it now says "BAD signature" instead of "No public key" 213 | 214 | ```shell 215 | $ gpg --delete-keys 3bf863cc 216 | ``` 217 | 218 |
219 | 220 | 221 | ### RPM packages 222 | 223 | Metadata is embedded on the outer layer of RPMs 224 | 225 | _Setup a test environment_ 226 | ```shell 227 | docker run -it rockylinux:8 /bin/bash -c "dnf install -y wget; bash" 228 | baseurl="https://developer.download.nvidia.com/compute/cuda/repos" 229 | wget $baseurl/rhel8/x86_64/cuda-11-0-11.0.1-1.x86_64.rpm 230 | wget $baseurl/rhel8/x86_64/D42D0685.pub 231 | ``` 232 | 233 | #### RPM package method 1 234 | 235 | ```shell 236 | rpm -Kv *.rpm 237 | ``` 238 | 239 | ##### Example 4 240 | 241 |
242 | Expand 243 | 244 | ```shell 245 | $ rpm -Kv cuda-11-0-11.0.1-1.x86_64.rpm 246 | cuda-11-0-11.0.1-1.x86_64.rpm: 247 | Header V4 RSA/SHA512 Signature, key ID d42d0685: NOKEY 248 | Header SHA1 digest: OK 249 | V4 RSA/SHA512 Signature, key ID d42d0685: NOKEY 250 | MD5 digest: O 251 | ``` 252 | 253 | then import the GPG public key 254 | 255 | ```shell 256 | $ rpm --import D42D0685.pub 257 | $ rpm -qa | grep gpg-pubkey 258 | gpg-pubkey-d42d0685-62589a51 259 | $ rpm -Kv cuda-11-0-11.0.1-1.x86_64.rpm 260 | cuda-11-0-11.0.1-1.x86_64.rpm: 261 | Header V4 RSA/SHA512 Signature, key ID d42d0685: OK 262 | Header SHA1 digest: OK 263 | V4 RSA/SHA512 Signature, key ID d42d0685: OK 264 | MD5 digest: OK 265 | ``` 266 | 267 | notice it now says "OK" 268 | 269 | ``` 270 | $ rpm --erase "gpg-pubkey-d42d0685*" 271 | ``` 272 | 273 |
274 | 275 | 276 | #### RPM package method 2 277 | 278 | ```shell 279 | rpm -qip *.rpm | grep ^Signature 280 | ``` 281 | 282 | ##### Example 5 283 | 284 |
285 | Expand 286 | 287 | ```shell 288 | $ rpm -qip cuda-11-0-11.0.1-1.x86_64.rpm | grep ^Signature 289 | warning: cuda-11-0-11.0.1-1.x86_64.rpm: Header V4 RSA/SHA512 Signature, key ID d42d0685: NOKEY 290 | Signature : RSA/SHA512, Sat Apr 23 05:50:03 2022, Key ID 9cd0a493d42d0685 291 | ``` 292 | 293 | then import the GPG public key 294 | 295 | ```shell 296 | $ rpm --import D42D0685.pub 297 | $ rpm -qa | grep gpg-pubkey 298 | gpg-pubkey-d42d0685-62589a51 299 | $ rpm -qip cuda-11-0-11.0.1-1.x86_64.rpm | grep ^Signature 300 | Signature : RSA/SHA512, Sat Apr 23 05:50:03 2022, Key ID 9cd0a493d42d0685 301 | ``` 302 | 303 | notice the warning error disappeared 304 | 305 |
306 | 307 | 308 | ## Verify repository 309 | 310 | ### Debian repo 311 | 312 | There are several metadata files, with the "entry point" either `InRelease` (concatenated with signature) or `Release` and `Release.gpg` (detached signature). 313 | Also there is `Packages` and `Packages.gz` (compressed) with contents that include the dependencies, descriptions, etc. 314 | 315 | _Setup a test environment_ 316 | ```shell 317 | setup='DEBIAN_FRONTEND=noninteractive apt-get install -y binutils gnupg wget ca-certificates sudo' 318 | docker run -it ubuntu:20.04 /bin/bash -c "apt-get update && $setup; bash" 319 | baseurl="https://developer.download.nvidia.com/compute/cuda/repos" 320 | wget $baseurl/ubuntu2004/x86_64/3bf863cc.pub 321 | wget $baseurl/ubuntu2004/x86_64/Release 322 | wget $baseurl/ubuntu2004/x86_64/Release.gpg 323 | wget $baseurl/ubuntu2004/x86_64/InRelease 324 | wget $baseurl/ubuntu2004/x86_64/cuda-ubuntu2004-keyring.gpg 325 | ``` 326 | 327 | #### Debian repo method 1 328 | 329 | ```shell 330 | gpg --verify Release.gpg Release 331 | ``` 332 | validate the detached signature: `Release.gpg` 333 | 334 | ##### Example 6 335 | 336 |
337 | Expand 338 | 339 | ```shell 340 | $ gpg --verify Release.gpg Release 341 | gpg: Signature made Wed Aug 17 19:06:30 2022 UTC 342 | gpg: using RSA key A4B469963BF863CC 343 | gpg: Can't check signature: No public key 344 | ``` 345 | 346 | then import the GPG public key 347 | 348 | ```shell 349 | $ gpg --import 3bf863cc.pub 350 | $ gpg --verify Release.gpg Release 351 | gpg: Signature made Wed Aug 17 19:06:30 2022 UTC 352 | gpg: using RSA key A4B469963BF863CC 353 | gpg: Good signature from "cudatools " [unknown] 354 | gpg: WARNING: This key is not certified with a trusted signature! 355 | gpg: There is no indication that the signature belongs to the owner. 356 | Primary key fingerprint: EB69 3B30 35CD 5710 E231 E123 A4B4 6996 3BF8 63CC 357 | ``` 358 | 359 |
360 | 361 | 362 | #### Debian repo method 2 363 | 364 | ```shell 365 | message=$(cat InRelease | sed -n '/-----BEGIN PGP SIGNATURE-----/q;p') 366 | detached=$(cat InRelease | sed -n '/-----BEGIN PGP SIGNATURE-----/,$p') 367 | gpg --verify <(echo "$detached") <(echo "$message") 368 | ``` 369 | validate the concatenated file: `InRelease` 370 | 371 | ##### Example 7 372 | 373 |
374 | Expand 375 | 376 | ```shell 377 | $ message=$(cat InRelease | sed -n '/-----BEGIN PGP SIGNATURE-----/q;p') 378 | $ detached=$(cat InRelease | sed -n '/-----BEGIN PGP SIGNATURE-----/,$p') 379 | $ gpg --verify <(echo "$detached") <(echo "$message") 380 | gpg: Signature made Wed Aug 17 19:06:30 2022 UTC 381 | gpg: using RSA key A4B469963BF863CC 382 | gpg: Can't check signature: No public key 383 | ``` 384 | 385 | then import the GPG public key 386 | 387 | ```shell 388 | $ gpg --verify <(echo "$detached") <(echo "$message") 389 | gpg: Signature made Wed Aug 17 19:06:30 2022 UTC 390 | gpg: using RSA key A4B469963BF863CC 391 | gpg: BAD signature from "cudatools " [unknown] 392 | $ gpg --delete-keys 3BF863CC 393 | ``` 394 | 395 |
396 | 397 | 398 | #### Debian repo method 3 399 | 400 | ```shell 401 | echo "deb [signed-by=/usr/share/keyrings/*-archive-keyring.gpg] https://path/to/repo/ /" | sudo tee /etc/apt/sources.list.d/my-repo.list 402 | sudo apt-get update 403 | ``` 404 | enable repo and refresh cached metadata 405 | 406 | ##### Example 8 407 | 408 |
409 | Expand 410 | 411 | ```shell 412 | $ mv cuda-ubuntu2004-keyring.gpg /usr/share/keyrings/cuda-archive-keyring.gpg 413 | $ echo "deb [signed-by=/usr/share/keyrings/cuda-archive-keyring.gpg] https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/ /" | sudo tee /etc/apt/sources.list.d/cuda-ubuntu2004-x86_64.list 414 | $ sudo apt-get update 415 | ``` 416 | 417 |
418 | 419 | 420 | ### RPM repo 421 | 422 | There are several metadata files under repodata/ with the "entry point" `repomd.xml` and `repomd.xml.asc` (detached signature). Also `repomd.xml.key` (GPG public key) is important. 423 | 424 | These include checksums, bytes, and timestamps for fine-grain `*-primary.{xml.gz,sqlite.bz2}` and etc. 425 | 426 | _Setup a test environment_ 427 | ```shell 428 | docker run -it rockylinux:8 /bin/bash -c "dnf install -y dnf-plugins-core wget sudo; bash" 429 | mkdir repodata 430 | baseurl="https://developer.download.nvidia.com/compute/cuda/repos" 431 | (cd repodata && wget $baseurl/rhel8/x86_64/repodata/repomd.xml) 432 | (cd repodata && wget $baseurl/rhel8/x86_64/repodata/repomd.xml.asc) 433 | (cd repodata && wget $baseurl/rhel8/x86_64/repodata/repomd.xml.key) 434 | ``` 435 | 436 | #### RPM repo method 1 437 | 438 | ```shell 439 | gpg --verify repodata/repomd.xml.asc repodata/repomd.xml 440 | ``` 441 | 442 | this is a manual way to validate detached signature 443 | 444 | ##### Example 9 445 | 446 |
447 | Expand 448 | 449 | ```shell 450 | $ gpg --verify repodata/repomd.xml.asc repodata/repomd.xml 451 | gpg: directory '/root/.gnupg' created 452 | gpg: keybox '/root/.gnupg/pubring.kbx' created 453 | gpg: Signature made Wed Aug 17 19:05:33 2022 UTC 454 | gpg: using RSA key 9CD0A493D42D0685 455 | gpg: Can't check signature: No public key 456 | ``` 457 | 458 | then import the GPG public key 459 | 460 | ```shell 461 | $ gpg --import repodata/repomd.xml.key 462 | gpg: key 9CD0A493D42D0685: public key "cudatools " imported 463 | gpg: Total number processed: 1 464 | gpg: imported: 1 465 | $ gpg --verify repodata/repomd.xml.asc repodata/repomd.xml 466 | gpg: Signature made Wed Aug 17 19:05:33 2022 UTC 467 | gpg: using RSA key 9CD0A493D42D0685 468 | gpg: Good signature from "cudatools " [unknown] 469 | gpg: WARNING: This key is not certified with a trusted signature! 470 | gpg: There is no indication that the signature belongs to the owner. 471 | Primary key fingerprint: 610C 7B14 E068 A878 070D A4E9 9CD0 A493 D42D 0685 472 | 473 | $ gpg --delete-keys 3D42D0685 474 | ``` 475 | 476 |
477 | 478 | 479 | #### RPM repo method 2 480 | 481 | ```shell 482 | sudo dnf config-manager --add-repo https://path/to/*.repo 483 | sudo dnf install some-package 484 | [...] 485 | Importing GPG key 0x000000: 486 | Userid : . . . 487 | Fingerprint: . . . 488 | From : /path/to/*.pub 489 | Is this ok [y/N]: 490 | ``` 491 | 492 | this uses the package manager to install (recommended) 493 | 494 | ##### Example 10 495 | 496 |
497 | Expand 498 | 499 | ```shell 500 | $ dnf config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel8/x86_64/cuda-rhel8.repo 501 | Adding repo from: https://developer.download.nvidia.com/compute/cuda/repos/rhel8/x86_64/cuda-rhel8.repo 502 | $ dnf install libnvjpeg-11-0 503 | [...] 504 | Importing GPG key 0xD42D0685: 505 | Userid : "cudatools " 506 | Fingerprint: 610C 7B14 E068 A878 070D A4E9 9CD0 A493 D42D 0685 507 | From : https://developer.download.nvidia.com/compute/cuda/repos/rhel8/x86_64/D42D0685.pub 508 | Is this ok [y/N]: y 509 | ``` 510 | 511 |
512 | 513 | -------------------------------------------------------------------------------- /repo-validate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC2001,SC2068,SC2086,SC2155 3 | # Copyright 2021, NVIDIA Corporation 4 | # SPDX-License-Identifier: MIT 5 | 6 | version=("") 7 | 8 | err() { echo "ERROR: $*"; print_mismatched; exit 1; } 9 | warn() { warnings+=("$1"); shift; echo "WARN: $*"; } 10 | 11 | usage() { 12 | echo "USAGE: $0 [distro] [arch] [version]" 13 | echo 14 | echo " PARAMETERS:" 15 | echo -e " --mirror=\t\t input directory" 16 | echo 17 | echo " OPTIONAL:" 18 | echo -e " --remote=[URL]\t compare mirror with server packages" 19 | echo -e " --distro=[rhel8,ubuntu2004]\t Linux distro name" 20 | echo -e " --arch=[x86_64,ppc64le,sbsa]\t CPU architecture" 21 | echo -e " --dryrun\t\t\t skip local files" 22 | echo -e " --version=<11.5.0>\t\t filter results by string" 23 | echo 24 | err "$*" 25 | } 26 | 27 | print_mismatched() 28 | { 29 | if [[ "${#mismatches[@]}" -gt 0 ]]; then 30 | echo 31 | echo "======================" 32 | for bad in ${mismatches[@]}; do 33 | echo "$bad" | sed "s|${inputDir}/||" 34 | done 35 | echo "Found ${#mismatches[@]} mismatches" 36 | fi 37 | } 38 | 39 | compare_bytes() 40 | { 41 | bytes=$(du -b "$1" 2>/dev/null | awk '{print $1}') 42 | if [[ "$bytes" != "$2" ]]; then 43 | [[ -z $fail ]] && echo && mismatches+=("$1") 44 | echo " -> size (bytes) mismatch" 45 | echo " - Expected: $2" 46 | echo " - Computed: $bytes" 47 | fail=$((fail+1)) 48 | fi 49 | } 50 | 51 | compare_sha256() 52 | { 53 | sha256=$(sha256sum "$1" 2>/dev/null | awk '{print $1}') 54 | if [[ -z "$2" ]]; then 55 | [[ -z $fail ]] && echo && mismatches+=("$1") 56 | echo " -> checksum (SHA256) empty" 57 | echo " - Computed: $sha256" 58 | elif [[ "$sha256" != "$2" ]]; then 59 | [[ -z $fail ]] && echo && mismatches+=("$1") 60 | echo " -> checksum (SHA256) mismatch" 61 | echo " - Expected: $2" 62 | echo " - Computed: $sha256" 63 | fail=$((fail+1)) 64 | fi 65 | } 66 | 67 | compare_sha1() 68 | { 69 | sha1=$(sha1sum "$1" 2>/dev/null | awk '{print $1}') 70 | if [[ -z "$2" ]]; then 71 | [[ -z $fail ]] && echo && mismatches+=("$1") 72 | echo " -> checksum (SHA1) empty" 73 | echo " - Computed: $sha1" 74 | elif [[ "$sha1" != "$2" ]]; then 75 | [[ -z $fail ]] && echo && mismatches+=("$1") 76 | echo " -> checksum (SHA1) mismatch" 77 | echo " - Expected: $2" 78 | echo " - Computed: $sha256" 79 | fail=$((fail+1)) 80 | fi 81 | } 82 | 83 | compare_rpmsign() 84 | { 85 | rpmsign=$(rpm -qip "$1" 2>&1 | grep "^Signature" | awk -F " : " '{print $NF}' | awk -F ", " '{print $2}') 86 | if [[ "$rpmsign" != "$2" ]]; then 87 | [[ -z $fail ]] && echo && mismatches+=("$1") 88 | echo " -> timestamp (rpmsign) mismatch" 89 | echo " - Expected: $2" 90 | echo " - Headers: $rpmsign" 91 | fail=$((fail+1)) 92 | fi 93 | } 94 | 95 | scan_primaries() 96 | { 97 | manifests=$(find ${repo} -name "*-primary.xml.gz") 98 | for primaryXML in $manifests; do 99 | lastUpdate=$(gunzip -c "$primaryXML" | grep " Found ${#rpmHistory[@]} repo postings" 103 | } 104 | 105 | xml_origin() 106 | { 107 | local filename=$1 108 | 109 | local filesize=$2 110 | local sizeA=$(echo "$filesize" | awk -F ":" '{print $1}') 111 | local sizeB=$(echo "$filesize" | awk -F ":" '{print $2}') 112 | 113 | local checksum=$3 114 | local hashA=$(echo "$checksum" | awk -F ":" '{print $1}') 115 | local hashB=$(echo "$checksum" | awk -F ":" '{print $2}') 116 | 117 | echo " -> Deep-scanning for $filename ..." 118 | local postcount=0 119 | 120 | for posting in $(echo "${rpmHistory[@]}" | sort -k1,1 -t: -n -r); do 121 | local postmark=$(echo "$posting" | awk -F ":" '{print $1}') 122 | local postmeta=$(echo "$posting" | awk -F ":" '{print $2}') 123 | local basemeta=$(basename "$postmeta" 2>/dev/null) 124 | local timestamp=$(date -d "@${postmark}") 125 | 126 | local oldBlock 127 | oldBlock=$(gunzip -c "$metadata" | awk '{printf "%s◬",$0} END {print ""}' | sed -e 's|||' | awk -F ">" '{print $NF}') 133 | 134 | if [[ "$mBytes" == "$sizeB" ]]; then 135 | continue 136 | elif [[ "$mSHA256" == "$hashB" ]]; then 137 | continue 138 | fi 139 | 140 | 141 | if [[ "$mSHA256" == "$hashA" ]]; then 142 | echo " [ORIGIN] $postmeta ($timestamp)" 143 | return 144 | elif [[ "$mBytes" == "$sizeA" ]]; then 145 | echo " [BADHASH] $posting ($timestamp)" 146 | else 147 | echo " - $basemeta [$mBytes] [$mSHA256] ($timestamp)" 148 | fi 149 | done 150 | 151 | echo " ::: No correct results found ($postcount)" 152 | } 153 | 154 | check_debian() 155 | { 156 | local metadata="$1" 157 | local filepath="$2" 158 | 159 | [[ -f "$metadata" ]] || err "Missing metadata file: $metadata" 160 | [[ -f "$filepath" ]] || err "Missing local package: $filepath" 161 | 162 | basename=$(basename "$filepath" 2>/dev/null) 163 | shortname=$(echo "$filepath" | sed "s|${repo}/||") 164 | 165 | local pkgBlock 166 | # Flatten package paragraphs into one-liners (uses special character as delimiter) 167 | pkgBlock=$(gunzip -c "$metadata" | sed 's|^$|#PACKAGE_BLOCK#|' | awk '{printf "%s◬",$0} END {print ""}' | sed -e 's|◬#PACKAGE_BLOCK#◬|\n|g' | grep "Filename: .*/${basename}" | sed 's|◬|\n|g') 168 | if [[ -z "$pkgBlock" ]]; then 169 | warn "$filepath" "no $basename entry in metadata ($metadata)" 170 | return 171 | fi 172 | 173 | mBytes=$(echo "$pkgBlock" | grep "^Size:" | awk '{print $NF}' | sort -u) 174 | mSHA256=$(echo "$pkgBlock" | grep "^SHA256:" | awk '{print $NF}' | sort -u) 175 | 176 | if [[ -n $dryrun ]]; then 177 | echo -n "$shortname" 178 | shorthash=$(echo "$mSHA256" | cut -c -10) 179 | echo " [$mBytes] [$shorthash]" 180 | else 181 | unset fail 182 | echo -n "$shortname" 183 | compare_bytes "$filepath" "$mBytes" 184 | compare_sha256 "$filepath" "$mSHA256" 185 | shorthash=$(echo "$sha256" | cut -c -10) 186 | 187 | if [[ -z $fail ]]; then 188 | echo " [$bytes] [$shorthash]" 189 | fi 190 | fi 191 | } 192 | 193 | check_rpm() 194 | { 195 | local metadata="$1" 196 | local filepath="$2" 197 | 198 | [[ -f "$metadata" ]] || err "Missing metadata file: $metadata" 199 | [[ -f "$filepath" ]] || err "Missing local package: $filepath" 200 | 201 | basename=$(basename "$filepath" 2>/dev/null) 202 | shortname=$(echo "$filepath" | sed "s|${repo}/||") 203 | 204 | local pkgBlock 205 | # Flatten package XML tags into one-liners (uses special character as delimiter) 206 | pkgBlock=$(gunzip -c "$metadata" | awk '{printf "%s◬",$0} END {print ""}' | sed -e 's|" | sed 's|◬|\n|g') 207 | if [[ -z "$pkgBlock" ]]; then 208 | warn "no $basename entry in metadata ($metadata)" 209 | return 210 | fi 211 | 212 | mBytes=$(echo "$pkgBlock" | grep '||' | awk -F ">" '{print $NF}' | sort -u) 214 | mSHA1=$(echo "$pkgBlock" | grep '||' | awk -F ">" '{print $NF}' | sort -u) 215 | mTimestamp=$(echo "$pkgBlock" | grep '