-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcodesign.sh
More file actions
executable file
·238 lines (216 loc) · 11.5 KB
/
Copy pathcodesign.sh
File metadata and controls
executable file
·238 lines (216 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
#!/usr/bin/env bash
# Signs all signable (Mach-O) binaries and bundles in the path specified by the
# first argument to the script using `rcodesign`.
#
# The signing identity is never named here: it is selected through
# `rcodesign`'s own configuration file, which expresses every backend it
# supports. This script only knows how to walk a Julia distribution and hand
# each signable entity to `rcodesign`.
#
# Configuration (environment variables):
# CODE_SIGNATURE_FLAGS Whitespace-separated code signature flags, each passed
# as its own `--code-signature-flags`. Empty passes
# none, leaving the matter to the configuration file.
# ENTITLEMENTS_FILE The literal `bundled` for the plist shipped next to
# this script, or the path of one of your own, relative
# paths resolving against the working directory. Empty
# passes no entitlements at all.
# RCODESIGN Path to an `rcodesign` binary. If unset,
# `get_rcodesign.sh` (next to this script) fetches the
# pinned binary.
# SIGNING_CONCURRENCY Max number of concurrent signers. Default 8.
# SIGNING_CONFIG_FILE Path to the `rcodesign` configuration file selecting
# the signer (`-C`). If empty, `rcodesign` falls back to
# its own default discovery.
# SIGNING_PROFILE Configuration profile to select within that file
# (`-P`). If empty, `rcodesign` loads `default`.
#
# Deliberately none of these are named `RCODESIGN_<something>`: `rcodesign`
# reads every *exported* variable under that prefix as a configuration key and
# rejects the ones it does not recognize, so a passthrough variable named that
# way would break signing rather than configure it.
#
# Closely derived from the main Julia distribution's signing pipeline:
# https://github.com/JuliaCI/julia-buildkite/pull/544 (utilities/macos/codesign.sh).
#
# Runs on bash 3.2, the version macOS ships as /bin/bash, which rules out
# `wait -n`: the concurrency throttle polls the job table instead.
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
# `${VAR-default}` rather than `${VAR:-default}` in the assignments that follow:
# an explicitly empty value means "pass nothing", which is how the flags and the
# entitlements are deferred entirely to the configuration file. Only an *unset*
# variable takes the default.
#
# `action.yml` is the source of truth for these defaults and always sets every
# variable, so the values repeated here only ever apply when the script is run
# directly. Keep the two in step.
CODE_SIGNATURE_FLAGS="${CODE_SIGNATURE_FLAGS-runtime}"
ENTITLEMENTS_FILE="${ENTITLEMENTS_FILE-bundled}"
SIGNING_CONFIG_FILE="${SIGNING_CONFIG_FILE-}"
SIGNING_PROFILE="${SIGNING_PROFILE-}"
# The exception: there is no such thing as signing with no concurrency, so an
# empty value falls back rather than meaning anything.
SIGNING_CONCURRENCY="${SIGNING_CONCURRENCY:-8}"
if [ "$#" -lt 1 ]; then
echo "Usage: $0 <directory-to-sign>" >&2
exit 1
fi
# Canonicalize to an absolute path so every path `find` emits below is absolute:
# `chmod`/`stat`/`rcodesign` would misparse a path beginning with `-` as a flag.
TARGET="$(cd "${1}" && pwd)"
if [ "${ENTITLEMENTS_FILE}" = "bundled" ]; then
ENTITLEMENTS_FILE="${SCRIPT_DIR}/entitlements.plist"
fi
if [ -n "${ENTITLEMENTS_FILE}" ] && [ ! -f "${ENTITLEMENTS_FILE}" ]; then
printf "\033[31mThe entitlements plist '%s' does not exist!\n\033[0m" "${ENTITLEMENTS_FILE}"
exit 1
fi
# `rcodesign` treats a missing configuration file as an empty one, so a mistyped
# path selects no signer at all rather than failing.
if [ -n "${SIGNING_CONFIG_FILE}" ] && [ ! -f "${SIGNING_CONFIG_FILE}" ]; then
printf "\033[31mThe rcodesign configuration file '%s' does not exist!\n\033[0m" "${SIGNING_CONFIG_FILE}"
exit 1
fi
RCODESIGN_BIN="$("${SCRIPT_DIR}/get_rcodesign.sh")"
# Every invocation below signs with the same settings, so assemble them once.
# The configuration file and profile are global arguments and precede the
# subcommand; the remainder are `sign` arguments and take precedence over the
# equivalents in the configuration file.
RCODESIGN_ARGUMENTS=()
if [ -n "${SIGNING_CONFIG_FILE}" ]; then
RCODESIGN_ARGUMENTS+=("--config-file" "${SIGNING_CONFIG_FILE}")
fi
if [ -n "${SIGNING_PROFILE}" ]; then
RCODESIGN_ARGUMENTS+=("--profile" "${SIGNING_PROFILE}")
fi
RCODESIGN_ARGUMENTS+=("sign")
# shellcheck disable=SC2086 # unquoted on purpose: a whitespace-separated list
for code_signature_flag in ${CODE_SIGNATURE_FLAGS}; do
RCODESIGN_ARGUMENTS+=("--code-signature-flags" "${code_signature_flag}")
done
if [ -n "${ENTITLEMENTS_FILE}" ]; then
RCODESIGN_ARGUMENTS+=("--entitlements-xml-file" "${ENTITLEMENTS_FILE}")
fi
# True if $1 begins with a Mach-O magic (thin LE/BE 32/64-bit, or a
# fat/universal archive). rcodesign signs Mach-O binaries only; the
# distribution tree also holds many *non*-Mach-O files (scripts, .jl, .tbd,
# Makefiles, headers, data) that the unfiltered `find` enumerates. rcodesign
# rejects those with "specified path is not of a recognized type"; they are
# plain data and need no code signature, so we skip them below.
is_macho() {
local magic
magic="$(od -An -tx1 -N4 -- "${1}" 2>/dev/null | tr -d ' \n')"
case "${magic}" in
feedface | cefaedfe | feedfacf | cffaedfe | cafebabe | bebafeca | cafebabf | bfbafeca) return 0 ;;
*) return 1 ;;
esac
}
sign_path() {
# Takes either a loose Mach-O or a bundle root. A `.framework`/`.app` must be
# signed as a bundle, not as the loose Mach-O inside it: the bundle carries a
# `_CodeSignature/CodeResources` seal over its contents and Apple's notary
# validates the bundle, so signing only the inner binary leaves the bundle
# unsealed and notarization rejects it with "the signature of the binary is
# invalid". rcodesign descends into nested bundles (a versioned framework's
# `Versions/A`) itself, applies the flags and entitlements to the nested main
# executable, and takes the signing identifier from the bundle's `Info.plist`.
local target="${1}" out rc=0 readonly_paths
readonly_paths="$(mktemp "${TMPFIFODIR}/readonly-paths.XXXXXX")"
# rcodesign rewrites each Mach-O in place and creates `_CodeSignature`, so
# anything Pkg installed read-only (0555/0444) needs the write bit for the
# duration. Only the u+w bit is toggled, and only on the paths that lacked it,
# so every other mode bit survives untouched. Symlinks are excluded because
# `-perm` tests the link while `chmod` follows it, which would strip the write
# bit from a target that has it.
find "${target}" \( -type f -o -type d \) ! -perm -u+w -print0 >"${readonly_paths}"
chmod -R u+w "${target}"
# rcodesign applies a secure timestamp by default and resolves the signing key
# through its configuration. It is chatty (~10 lines per file); across many
# files signed in parallel that floods and interleaves the log, so capture
# output and only emit it on failure.
if ! out="$("${RCODESIGN_BIN}" "${RCODESIGN_ARGUMENTS[@]}" "${target}" 2>&1)"; then
echo "ERROR: rcodesign failed for ${target}:" >&2
echo "${out}" >&2
rc=1
fi
# Mode fidelity is the point of the capture above, so a failed restore is a
# failed signing operation. Directories are restored before the files they
# contain: dropping a directory's write bit blocks creating and removing
# entries in it, not `chmod` on entries that already exist.
if [ -s "${readonly_paths}" ]; then
xargs -0 chmod u-w <"${readonly_paths}" || rc=1
fi
rm -f "${readonly_paths}"
return "${rc}"
}
# A pipe propagates the list of binaries from `find` into the signing loop; a
# scratch dir records signer failures out of band. Both are cleaned up on exit.
# `${VAR:-}` guards the trap against firing before the assignments below (e.g.
# on an early `mktemp`/`mkfifo` failure under `set -u`).
TMPFIFODIR=""
STATUS_DIR=""
trap 'rm -rf "${TMPFIFODIR:-}" "${STATUS_DIR:-}"' EXIT
TMPFIFODIR="$(mktemp -d)"
STATUS_DIR="$(mktemp -d)"
mkfifo "${TMPFIFODIR}/findpipe"
# Emit bundle roots and loose files on one stream: `-prune` stops the descent at
# a `.framework`/`.app` so its interior never reaches the loose-file branch —
# re-signing a Mach-O inside a sealed bundle would invalidate the seal. `.dSYM`
# bundles are not pruned; the path check below skips their interiors.
# Capture the writer's PID so the concurrency throttle below can count signers only.
find "${TARGET}" \
\( -type d \( -name "*.framework" -o -name "*.app" \) -prune -print0 \) -o \
\( -type f -print0 \) >"${TMPFIFODIR}/findpipe" &
FIND_PID=$!
# A non-empty STATUS_DIR after the run means at least one signer failed: the
# trailing `wait` returns 0 regardless, so failures are recorded out-of-band
# rather than silently swallowed (never ship a partially-signed distribution).
# Blocks until a worker slot frees up, so the fan-out onto the signing backend
# stays bounded. `jobs -rp` also lists the background `find` writer while it is
# still producing paths, so exclude it to count signers only (otherwise the bound
# would be one less than asked for). A remote signature dwarfs the poll interval.
wait_for_free_slot() {
while (("$(jobs -rp | grep -vc "^${FIND_PID}\$")" >= SIGNING_CONCURRENCY)); do sleep 0.1; done
}
echo "Code signing directory: '${TARGET}' (up to ${SIGNING_CONCURRENCY} concurrent)."
NUMBER_OF_FILES_SIGNED=0
NUMBER_OF_FILES_SKIPPED=0
NUMBER_OF_BUNDLES_SIGNED=0
while IFS= read -r -d '' file_to_sign; do
# A directory on this stream is a pruned bundle root; everything else is a
# loose file to be filtered below.
if [ -d "${file_to_sign}" ]; then
wait_for_free_slot
{ sign_path "${file_to_sign}" || touch "${STATUS_DIR}/fail.bundle.${NUMBER_OF_BUNDLES_SIGNED}"; } &
NUMBER_OF_BUNDLES_SIGNED="$((NUMBER_OF_BUNDLES_SIGNED + 1))"
continue
fi
# Skip files that need no signature -- either not Mach-O, or Mach-O of a kind
# rcodesign cannot (and need not) sign:
# - non-Mach-O (scripts, .jl, .tbd, Makefiles, ...): rejected as
# "specified path is not of a recognized type".
# - static archives (*.a): a universal/"fat" archive carries a fat magic so
# it passes is_macho, but it is an `ar` archive, not a signable binary --
# rcodesign panics parsing it. Static libs are link-time input.
# - relocatable object files (*.o): Mach-O, but their __LINKEDIT is not the
# final segment, so no signature can be appended ("__LINKEDIT isn't final
# Mach-O segment"). They are link-time input, not loaded at runtime.
# - dSYM debug bundles (*.dSYM/**): the DWARF payload is Mach-O with the same
# non-final-__LINKEDIT layout; Apple's notary ignores dSYMs regardless.
if [[ "${file_to_sign}" == *.a || "${file_to_sign}" == *.o || "${file_to_sign}" == *.dSYM/* ]] \
|| ! is_macho "${file_to_sign}"; then
NUMBER_OF_FILES_SKIPPED="$((NUMBER_OF_FILES_SKIPPED + 1))"
continue
fi
wait_for_free_slot
{ sign_path "${file_to_sign}" || touch "${STATUS_DIR}/fail.${NUMBER_OF_FILES_SIGNED}"; } &
NUMBER_OF_FILES_SIGNED="$((NUMBER_OF_FILES_SIGNED + 1))"
done <"${TMPFIFODIR}/findpipe"
wait
NUMBER_OF_OPERATIONS_FAILED="$(find "${STATUS_DIR}" -type f | wc -l | tr -d ' ')"
if [ "${NUMBER_OF_OPERATIONS_FAILED}" -ne 0 ]; then
echo "ERROR: ${NUMBER_OF_OPERATIONS_FAILED} of $((NUMBER_OF_FILES_SIGNED + NUMBER_OF_BUNDLES_SIGNED)) codesign operations failed" >&2
exit 1
fi
echo "Codesigned ${NUMBER_OF_FILES_SIGNED} files and ${NUMBER_OF_BUNDLES_SIGNED} bundles (skipped ${NUMBER_OF_FILES_SKIPPED} unsignable)"