# shellcheck shell=bash # Shared helpers for the self-built ArgyllCMS pipeline. SOURCED (never # executed) by build_argyll.sh, tool/verify_argyll_helpers.sh and # tool/package_argyll_source.sh. Loads the pins (argyll_pins.sh, or the file # named by ARGYLL_PINS_FILE in tests) and defines: # # argyll_sha256 -> prints the file's SHA-256 # argyll_code_sha256 -> prints the SHA-256 of the binary # with its code signature removed # argyll_fetch [local-file] # -> prints the path of a verified copy # argyll_ref_code_sha256 -> prints the pinned code digest # argyll_license_sha256 -> prints the pinned digest of a # staged license text # # The only network access anywhere in the pipeline is argyll_fetch's curl of a # PINNED https URL, and only when neither a local file nor a verified cached # copy exists. ARGYLL_NO_DOWNLOAD=1 turns even that off. ARGYLL_SIDECAR_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ARGYLL_PINS="${ARGYLL_PINS_FILE:-$ARGYLL_SIDECAR_DIR/argyll_pins.sh}" if [[ ! -f "$ARGYLL_PINS" ]]; then echo "error: Argyll pins file not found: $ARGYLL_PINS" >&2 return 1 2>/dev/null || exit 1 fi # shellcheck source=argyll_pins.sh source "$ARGYLL_PINS" ARGYLL_REPO_ROOT="$(cd "$ARGYLL_SIDECAR_DIR/../../../.." && pwd)" ARGYLL_CACHE_DIR="${ARGYLL_CACHE_DIR:-$ARGYLL_REPO_ROOT/packages/app/build/argyll/cache}" # The committed notice that ships as Contents/Resources/licenses/argyllcms/NOTICE.txt. ARGYLL_NOTICE_FILE="${ARGYLL_NOTICE_FILE:-$ARGYLL_SIDECAR_DIR/licenses/argyllcms/NOTICE.txt}" argyll_sha256() { shasum -a 256 "$1" | awk '{print $1}' } # The "code digest" of a thin 64-bit Mach-O: the SHA-256 of a copy with its code # signature removed AND its __LINKEDIT segment's vmsize field zeroed. The file # itself is never touched. # # Why both steps: signing appends a signature blob to __LINKEDIT and # `codesign --remove-signature` cuts it off again, but when a signature is big # enough to grow __LINKEDIT past a page (a Developer ID signature with its # certificate chain, or a large entitlements blob), the segment's vmsize is left # at the grown value. Measured on spotread: linker-signed, ad-hoc and # ad-hoc+runtime copies strip to the same bytes, but a copy signed with a large # entitlements blob strips to bytes that differ in that one field. Zeroing it # makes every signed form of one build agree; every other byte (all code, data, # load commands and symbol tables) is still compared. argyll_code_sha256() { local src="$1" tmp tmp="$(mktemp -t argyll-code)" || return 1 if ! cp "$src" "$tmp"; then rm -f "$tmp" return 1 fi chmod u+w "$tmp" if codesign -d "$tmp" >/dev/null 2>&1; then if ! codesign --remove-signature "$tmp" >/dev/null 2>&1; then rm -f "$tmp" return 1 fi fi if ! /usr/bin/perl -e ' use strict; use warnings; my $f = shift; open(my $fh, "+<:raw", $f) or die "open: $!\n"; read($fh, my $hdr, 32) == 32 or die "short Mach-O header\n"; my ($magic, undef, undef, undef, $ncmds, $sizeofcmds) = unpack("V6", $hdr); die "not a thin 64-bit Mach-O\n" unless $magic == 0xfeedfacf; my ($off, $end, $found) = (32, 32 + $sizeofcmds, 0); for (1 .. $ncmds) { die "load commands overrun\n" if $off + 8 > $end; seek($fh, $off, 0); read($fh, my $lc, 8) == 8 or die "short load command\n"; my ($cmd, $size) = unpack("V V", $lc); die "bad load command size\n" if $size < 8 || $off + $size > $end; if ($cmd == 0x19) { # LC_SEGMENT_64 seek($fh, $off + 8, 0); read($fh, my $name, 16) == 16 or die "short segment\n"; $name =~ s/\0.*//s; if ($name eq "__LINKEDIT") { seek($fh, $off + 32, 0); print {$fh} pack("Q<", 0); $found++; } } $off += $size; } close($fh) or die "close: $!\n"; die "expected one __LINKEDIT segment, found $found\n" unless $found == 1; ' "$tmp"; then rm -f "$tmp" return 1 fi argyll_sha256 "$tmp" rm -f "$tmp" } argyll_ref_code_sha256() { local var="ARGYLL_REF_CODE_SHA256_$1" printf '%s' "${!var:-}" } # argyll_license_sha256 -> the pinned SHA-256 of that license # text (e.g. icclib-License4.txt -> ARGYLL_LIC_SHA256_icclib_License4), or # nothing when the pins file has none. argyll_license_sha256() { local base="${1%.txt}" local var="ARGYLL_LIC_SHA256_${base//-/_}" printf '%s' "${!var:-}" } # argyll_fetch [local-file] # Prints the path of a copy whose SHA-256 equals . Order: the local file # (must match, never silently replaced by a download), then the cache, then one # pinned https download into the cache. Any mismatch is a hard error. argyll_fetch() { local url="$1" name="$2" want="$3" local_file="${4:-}" got cached part if [[ -n "$local_file" ]]; then if [[ ! -f "$local_file" ]]; then echo "error: $local_file does not exist" >&2 return 1 fi got="$(argyll_sha256 "$local_file")" if [[ "$got" != "$want" ]]; then echo "error: $local_file has SHA-256 $got, expected the pinned $want" >&2 return 1 fi # Absolute, so a caller that cd's into a work dir (build_argyll.sh unpacks # there) still finds the file it verified. printf '%s/%s\n' "$(cd "$(dirname "$local_file")" && pwd)" "$(basename "$local_file")" return 0 fi cached="$ARGYLL_CACHE_DIR/$name" if [[ -f "$cached" ]]; then got="$(argyll_sha256 "$cached")" if [[ "$got" == "$want" ]]; then printf '%s\n' "$cached" return 0 fi echo "warning: cached $cached has SHA-256 $got, not the pinned $want; discarding it" >&2 rm -f "$cached" fi if [[ "${ARGYLL_NO_DOWNLOAD:-0}" == "1" ]]; then echo "error: $name is not cached at $cached and ARGYLL_NO_DOWNLOAD=1; pass the file explicitly" >&2 return 1 fi case "$url" in https://*) ;; *) echo "error: refusing to download $name from a non-https URL: $url" >&2 return 1 ;; esac mkdir -p "$ARGYLL_CACHE_DIR" || return 1 part="$cached.part" rm -f "$part" echo "==> argyll: downloading $url" >&2 if ! curl --proto '=https' --tlsv1.2 -fsSL --retry 2 -o "$part" "$url"; then rm -f "$part" echo "error: download of $url failed" >&2 return 1 fi got="$(argyll_sha256 "$part")" if [[ "$got" != "$want" ]]; then rm -f "$part" echo "error: $url downloaded with SHA-256 $got, expected the pinned $want" >&2 return 1 fi mv -f "$part" "$cached" printf '%s\n' "$cached" }