Skip to content

Commit 2f64c3e

Browse files
committed
feat: Mac hardened signing on signingscript
1 parent 37634c5 commit 2f64c3e

22 files changed

+443
-48
lines changed

signingscript/Dockerfile

+11-11
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ RUN groupadd --gid 10001 app && \
1111

1212
# Copy only required folders
1313
COPY ["signingscript", "/app/signingscript/"]
14+
COPY ["scriptworker_client", "/app/scriptworker_client/"]
1415
COPY ["configloader", "/app/configloader/"]
1516
COPY ["docker.d", "/app/docker.d/"]
1617
COPY ["vendored", "/app/vendored/"]
@@ -21,17 +22,15 @@ COPY ["version.jso[n]", "/app/"]
2122
# Change owner of /app to app:app
2223
# Install msix
2324
# Install rcodesign
24-
RUN chown -R app:app /app && \
25-
cd /app/signingscript/docker.d && \
26-
bash build_msix_packaging.sh && \
27-
cp msix-packaging/.vs/bin/makemsix /usr/bin && \
28-
cp msix-packaging/.vs/lib/libmsix.so /usr/lib && \
29-
cd .. && \
30-
rm -rf msix-packaging && \
31-
wget -qO- \
32-
https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-x86_64-unknown-linux-musl.tar.gz \
33-
| tar xvz -C /usr/bin --transform 's/.*\///g' --wildcards --no-anchored 'rcodesign' && \
34-
chmod +x /usr/bin/rcodesign
25+
RUN chown -R app:app /app \
26+
&& cd /app/scriptworker_client \
27+
&& pip install /app/scriptworker_client \
28+
&& pip install -r requirements/base.txt \
29+
&& pip install . \
30+
&& cd /app/signingscript/docker.d \
31+
&& bash build_libdmg_hfsplus.sh /usr/bin \
32+
&& bash build_rcodesign.sh /usr/bin \
33+
&& bash build_msix_packaging.sh
3534

3635
# Set user and workdir
3736
USER app
@@ -40,6 +39,7 @@ WORKDIR /app
4039
# Install signingscript + configloader + widevine
4140
RUN python -m venv /app \
4241
&& cd signingscript \
42+
&& /app/bin/pip install /app/scriptworker_client \
4343
&& /app/bin/pip install -r requirements/base.txt \
4444
&& /app/bin/pip install . \
4545
&& python -m venv /app/configloader_venv \
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
$let:
2+
scope_prefix:
3+
$match:
4+
'COT_PRODUCT == "firefox"': 'project:releng:signing:'
5+
'COT_PRODUCT == "thunderbird"': 'project:comm:thunderbird:releng:signing:'
6+
'COT_PRODUCT == "mozillavpn"': 'project:mozillavpn:releng:signing:'
7+
'COT_PRODUCT == "adhoc"': 'project:adhoc:releng:signing:'
8+
in:
9+
$merge:
10+
$match:
11+
'ENV == "prod" && scope_prefix':
12+
'${scope_prefix[0]}cert:release-apple-signing':
13+
- "app_credentials": {"$eval": "APPLE_APP_SIGNING_CREDENTIALS"}
14+
"installer_credentials": {"$eval": "APPLE_INSTALLER_SIGNING_CREDENTIALS"}
15+
"password": {"$eval": "APPLE_SIGNING_CREDS_PASSWORD"}
16+
'ENV != "prod" && scope_prefix':
17+
'${scope_prefix[0]}cert:dep-apple-signing':
18+
- "app_credentials": {"$eval": "APPLE_APP_SIGNING_DEP_CREDENTIALS"}
19+
"installer_credentials": {"$eval": "APPLE_INSTALLER_SIGNING_DEP_CREDENTIALS"}
20+
"password": {"$eval": "APPLE_SIGNING_DEP_CREDS_PASSWORD"}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/bin/bash
2+
set -x -e -v
3+
4+
# This script is for building libdmg-hfsplus to get the `dmg` and `hfsplus`
5+
# tools for handling DMG archives on Linux.
6+
7+
DEST=$1
8+
if [ -d "$DEST" ]; then
9+
echo "Binaries will be installed to: $DEST"
10+
else
11+
echo "Destination directory doesn't exist!"
12+
exit 1
13+
fi
14+
15+
git clone --depth=1 --branch mozilla --single-branch https://github.com/mozilla/libdmg-hfsplus/ libdmg-hfsplus
16+
17+
pushd libdmg-hfsplus
18+
19+
# The openssl libraries in the sysroot cannot be linked in a PIE executable so we use -no-pie
20+
cmake \
21+
-DOPENSSL_USE_STATIC_LIBS=1 \
22+
-DCMAKE_EXE_LINKER_FLAGS=-no-pie \
23+
.
24+
25+
make VERBOSE=1 -j$(nproc)
26+
27+
# We only need the dmg and hfsplus tools.
28+
strip dmg/dmg hfs/hfsplus
29+
cp dmg/dmg hfs/hfsplus "$DEST"
30+
31+
popd
32+
rm -rf libdmg-hfsplus
33+
echo "Done."

signingscript/docker.d/build_msix_packaging.sh

+5
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,8 @@ cd msix-packaging
88
./makelinux.sh --pack
99

1010
cd ..
11+
12+
cp msix-packaging/.vs/bin/makemsix /usr/bin
13+
cp msix-packaging/.vs/lib/libmsix.so /usr/lib
14+
15+
rm -rf msix-packaging
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/bin/bash
2+
set -x -e -v
3+
4+
DEST=$1
5+
if [ -d "$DEST" ]; then
6+
echo "Binaries will be installed to: $DEST"
7+
else
8+
echo "Destination directory doesn't exist!"
9+
exit 1
10+
fi
11+
12+
13+
wget -qO- https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.26.0/apple-codesign-0.26.0-x86_64-unknown-linux-musl.tar.gz \
14+
| tar xvz -C "$DEST" --transform 's/.*\///g' --wildcards --no-anchored 'rcodesign'
15+
16+
chmod +x "${DEST}/rcodesign"

signingscript/docker.d/init_worker.sh

+4-2
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ test_var_set 'PROJECT_NAME'
2121
test_var_set 'PUBLIC_IP'
2222
test_var_set 'TEMPLATE_DIR'
2323

24-
export DMG_PATH=$APP_DIR/signingscript/files/dmg
25-
export HFSPLUS_PATH=$APP_DIR/signingscript/files/hfsplus
24+
export DMG_PATH=/usr/bin/dmg
25+
export HFSPLUS_PATH=/usr/bin/hfsplus
2626

2727
export PASSWORDS_PATH=$CONFIG_DIR/passwords.json
2828
export APPLE_NOTARIZATION_CREDS_PATH=$CONFIG_DIR/apple_notarization_creds.json
29+
export APPLE_SIGNING_CONFIG_PATH=$CONFIG_DIR/apple_signing_config.json
2930
export GPG_PUBKEY_PATH=$APP_DIR/signingscript/src/signingscript/data/gpg_pubkey_dep.asc
3031
export WIDEVINE_CERT_PATH=$CONFIG_DIR/widevine.crt
3132
export AUTHENTICODE_TIMESTAMP_STYLE=old
@@ -260,3 +261,4 @@ esac
260261

261262
$CONFIG_LOADER $TEMPLATE_DIR/passwords.yml $PASSWORDS_PATH
262263
$CONFIG_LOADER $TEMPLATE_DIR/apple_notarization_creds.yml $APPLE_NOTARIZATION_CREDS_PATH
264+
$CONFIG_LOADER $TEMPLATE_DIR/apple_signing_creds.yml $APPLE_SIGNING_CONFIG_PATH

signingscript/docker.d/worker.yml

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ verbose: { "$eval": "VERBOSE == 'true'" }
44
my_ip: { "$eval": "PUBLIC_IP" }
55
autograph_configs: { "$eval": "PASSWORDS_PATH" }
66
apple_notarization_configs: { "$eval": "APPLE_NOTARIZATION_CREDS_PATH" }
7+
apple_signing_configs: { "$eval": "APPLE_SIGNING_CONFIG_PATH" }
78
taskcluster_scope_prefixes:
89
$flatten:
910
$match:

signingscript/files/README

-3
This file was deleted.

signingscript/files/dmg

-153 KB
Binary file not shown.

signingscript/files/hfsplus

-104 KB
Binary file not shown.

signingscript/setup.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
with open(os.path.join(os.path.abspath(os.path.dirname(__file__)), "version.txt")) as f:
66
version = f.read().rstrip()
77

8-
install_requires = ["arrow", "mar", "scriptworker", "taskcluster", "mohawk", "winsign", "macholib"]
8+
with open(os.path.join(os.path.abspath(os.path.dirname(__file__)), "requirements", "base.in")) as f:
9+
install_requires = ["scriptworker_client"] + f.readlines()
910

1011
setup(
1112
name="signingscript",

signingscript/src/signingscript/rcodesign.py

+136-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
#!/usr/bin/env python
22
"""Functions that interface with rcodesign"""
33
import asyncio
4+
from collections import namedtuple
45
import logging
6+
import os
57
import re
8+
from glob import glob
9+
from shutil import copy2
10+
11+
from scriptworker_client.aio import download_file, raise_future_exceptions, retry_async
12+
from scriptworker_client.exceptions import DownloadError
613
from signingscript.exceptions import SigningScriptError
714

815
log = logging.getLogger(__name__)
@@ -41,13 +48,14 @@ async def _execute_command(command):
4148
stderr = (await proc.stderr.readline()).decode("utf-8").rstrip()
4249
if stderr:
4350
# Unfortunately a lot of outputs from rcodesign come out to stderr
44-
log.warn(stderr)
51+
log.warning(stderr)
4552
output_lines.append(stderr)
4653

4754
exitcode = await proc.wait()
4855
log.info("exitcode {}".format(exitcode))
4956
return exitcode, output_lines
5057

58+
5159
def find_submission_id(logs):
5260
"""Given notarization logs, find and return the submission id
5361
Args:
@@ -128,6 +136,7 @@ async def rcodesign_check_result(logs):
128136
raise RCodesignError("Notarization failed!")
129137
return
130138

139+
131140
async def rcodesign_staple(path):
132141
"""Staples a given app
133142
Args:
@@ -146,3 +155,129 @@ async def rcodesign_staple(path):
146155
if exitcode > 0:
147156
raise RCodesignError(f"Error stapling notarization. Exit code {exitcode}")
148157
return
158+
159+
160+
def _create_empty_entitlements_file(dest):
161+
contents = """<?xml version="1.0" encoding="UTF-8"?>
162+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
163+
<plist version="1.0">
164+
<dict>
165+
</dict>
166+
</plist>
167+
""".lstrip()
168+
with open(dest, "wt") as fd:
169+
fd.writelines(contents)
170+
171+
172+
async def _download_entitlements(hardened_sign_config, workdir):
173+
"""Download entitlements listed in the hardened signing config
174+
Args:
175+
hardened_sign_config (list): hardened signing configs
176+
workdir (str): current work directory where entitlements will be saved
177+
178+
Returns:
179+
Map of url -> local file location
180+
"""
181+
empty_file = os.path.join(workdir, "0-empty.xml")
182+
_create_empty_entitlements_file(empty_file)
183+
# rcodesign requires us to specify an "empty" entitlements file
184+
url_map = {None: empty_file}
185+
186+
# Unique urls to be downloaded
187+
urls_to_download = set([i["entitlements"] for i in hardened_sign_config if "entitlements" in i])
188+
# If nothing found, skip
189+
if not urls_to_download:
190+
log.warn("No entitlements urls provided! Skipping download.")
191+
return url_map
192+
193+
futures = []
194+
for index, url in enumerate(urls_to_download, start=1):
195+
# Prefix filename with an index in case filenames are the same
196+
filename = "{}-{}".format(index, url.split("/")[-1])
197+
dest = os.path.join(workdir, filename)
198+
url_map[url] = dest
199+
log.info(f"Downloading resource: {filename} from {url}")
200+
futures.append(
201+
asyncio.ensure_future(
202+
retry_async(
203+
download_file,
204+
retry_exceptions=(DownloadError, TimeoutError),
205+
args=(url, dest),
206+
attempts=5,
207+
)
208+
)
209+
)
210+
await raise_future_exceptions(futures)
211+
return url_map
212+
213+
214+
EntitlementEntry = namedtuple(
215+
"EntitlementEntry",
216+
["file", "entitlement", "runtime"],
217+
)
218+
219+
def _get_entitlements_args(hardened_sign_config, path, entitlements_map):
220+
"""Builds the list of entitlements based on files in path
221+
222+
Args:
223+
hardened_sign_config (list): hardened signing configuration
224+
path (str): path to app
225+
"""
226+
entries = []
227+
228+
for config in hardened_sign_config:
229+
entitlement_path = entitlements_map.get(config.get("entitlements"))
230+
for path_glob in config["globs"]:
231+
separator = ""
232+
if not path_glob.startswith("/"):
233+
separator = "/"
234+
# Join incoming glob with root of app path
235+
full_path_glob = path + separator + path_glob
236+
for binary_path in glob(full_path_glob, recursive=True):
237+
# Get relative path
238+
relative_path = os.path.relpath(binary_path, path)
239+
# Append "<binary path>:<entitlement>" to list of args
240+
entries.append(
241+
EntitlementEntry(
242+
file=relative_path,
243+
entitlement=entitlement_path,
244+
runtime=config.get("runtime"),
245+
)
246+
)
247+
248+
return entries
249+
250+
251+
async def rcodesign_sign(workdir, path, creds_path, creds_pass_path, hardened_sign_config=[]):
252+
"""Signs a given app
253+
Args:
254+
workdir (str): Path to work directory
255+
path (str): Path to be signed
256+
creds_path (str): Path to credentials file
257+
creds_pass_path (str): Path to credentials password file
258+
hardened_sign_config (list): Hardened signing configuration
259+
260+
Returns:
261+
(Tuple) exit code, log lines
262+
"""
263+
# TODO: Validate and sanitize input
264+
command = [
265+
"rcodesign",
266+
"sign",
267+
"--code-signature-flags=runtime",
268+
f"--p12-file={creds_path}",
269+
f"--p12-password-file={creds_pass_path}",
270+
]
271+
272+
entitlements_map = await _download_entitlements(hardened_sign_config, workdir)
273+
file_entitlements = _get_entitlements_args(hardened_sign_config, path, entitlements_map)
274+
275+
for entry in file_entitlements:
276+
if entry.runtime:
277+
flags_arg = f"--code-signature-flags=runtime:{entry.file}"
278+
command.append(flags_arg)
279+
entitlement_arg = f"--entitlements-xml-path={entry.file}:{entry.entitlement}"
280+
command.append(entitlement_arg)
281+
282+
command.append(path)
283+
await _execute_command(command)

0 commit comments

Comments
 (0)