Skip to content

Commit 739dcdb

Browse files
committed
feat: Mac hardened signing on signingscript
1 parent 96fb871 commit 739dcdb

25 files changed

+545
-51
lines changed

signingscript/Dockerfile

+12-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/"]
@@ -19,19 +20,18 @@ COPY ["vendored", "/app/vendored/"]
1920
COPY ["version.jso[n]", "/app/"]
2021

2122
# Change owner of /app to app:app
23+
# Build and install libdmg_hfsplus
2224
# Install msix
2325
# 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
26+
RUN chown -R app:app /app \
27+
&& cd /app/scriptworker_client \
28+
&& pip install /app/scriptworker_client \
29+
&& pip install -r requirements/base.txt \
30+
&& pip install . \
31+
&& cd /app/signingscript/docker.d \
32+
&& bash build_libdmg_hfsplus.sh /usr/bin \
33+
&& bash build_rcodesign.sh /usr/bin \
34+
&& bash build_msix_packaging.sh
3535

3636
# Set user and workdir
3737
USER app
@@ -40,6 +40,7 @@ WORKDIR /app
4040
# Install signingscript + configloader + widevine
4141
RUN python -m venv /app \
4242
&& cd signingscript \
43+
&& /app/bin/pip install /app/scriptworker_client \
4344
&& /app/bin/pip install -r requirements/base.txt \
4445
&& /app/bin/pip install . \
4546
&& python -m venv /app/configloader_venv \
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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-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+
'${scope_prefix[0]}cert:nightly-signing':
17+
- "app_credentials": {"$eval": "APPLE_APP_SIGNING_CREDENTIALS"}
18+
"installer_credentials": {"$eval": "APPLE_INSTALLER_SIGNING_CREDENTIALS"}
19+
"password": {"$eval": "APPLE_SIGNING_CREDS_PASSWORD"}
20+
'ENV != "prod" && scope_prefix':
21+
'${scope_prefix[0]}cert:dep-signing':
22+
- "app_credentials": {"$eval": "APPLE_APP_SIGNING_DEP_CREDENTIALS"}
23+
"installer_credentials": {"$eval": "APPLE_INSTALLER_SIGNING_DEP_CREDENTIALS"}
24+
"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",
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import logging
2+
import os
3+
from shutil import copy2
4+
5+
from signingscript.exceptions import SigningScriptError
6+
7+
log = logging.getLogger(__name__)
8+
9+
10+
PROVISIONING_PROFILE_FILENAMES = {
11+
"firefox": "orgmozillafirefox.provisionprofile",
12+
"devedition": "orgmozillafirefoxdeveloperedition.provisionprofile",
13+
"nightly": "orgmozillanightly.provisionprofile",
14+
}
15+
16+
17+
def copy_provisioning_profiles(bundlepath, configs):
18+
"""Copy provisioning profiles inside bundle
19+
Args:
20+
bundlepath (str): The absolute path to the app bundle
21+
configs (list): The list of configs with schema [{"profile_name": str, "target_path": str}]
22+
"""
23+
for cfg in configs:
24+
profile_name = cfg.get("profile_name")
25+
target_path = cfg.get("target_path")
26+
if not profile_name or not target_path:
27+
raise SigningScriptError(f"profile_name and target_path are required. Got: {cfg}")
28+
29+
if profile_name not in PROVISIONING_PROFILE_FILENAMES.values():
30+
raise SigningScriptError(f"profile_name not allowed: {profile_name}")
31+
32+
profile_path = os.path.join(os.path.dirname(__file__), "data", profile_name)
33+
if not os.path.exists(profile_path):
34+
raise SigningScriptError(f"Provisioning profile not found: {profile_name}")
35+
36+
# Resolve absolute destination path
37+
target_abs_path = os.path.join(bundlepath, target_path if target_path[0] != "/" else target_path[1:])
38+
if os.path.exists(target_abs_path):
39+
log.warning("Provisioning profile at {target_path} already exists, overriding.")
40+
41+
log.info(f"Copying {profile_name} to {target_abs_path}")
42+
copy2(profile_path, target_abs_path)
Binary file not shown.

signingscript/src/signingscript/rcodesign.py

+140-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
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+
10+
from scriptworker_client.aio import download_file, raise_future_exceptions, retry_async
11+
from scriptworker_client.exceptions import DownloadError
612
from signingscript.exceptions import SigningScriptError
713

814
log = logging.getLogger(__name__)
@@ -41,13 +47,14 @@ async def _execute_command(command):
4147
stderr = (await proc.stderr.readline()).decode("utf-8").rstrip()
4248
if stderr:
4349
# Unfortunately a lot of outputs from rcodesign come out to stderr
44-
log.warn(stderr)
50+
log.warning(stderr)
4551
output_lines.append(stderr)
4652

4753
exitcode = await proc.wait()
4854
log.info("exitcode {}".format(exitcode))
4955
return exitcode, output_lines
5056

57+
5158
def find_submission_id(logs):
5259
"""Given notarization logs, find and return the submission id
5360
Args:
@@ -128,6 +135,7 @@ async def rcodesign_check_result(logs):
128135
raise RCodesignError("Notarization failed!")
129136
return
130137

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

0 commit comments

Comments
 (0)