Skip to content

Commit f676f83

Browse files
committed
feat: Mac hardened signing on signingscript
1 parent b2d06c7 commit f676f83

15 files changed

+355
-31
lines changed

signingscript/Dockerfile

+7-1
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/"]
@@ -22,14 +23,18 @@ COPY ["version.jso[n]", "/app/"]
2223
# Install msix
2324
# Install rcodesign
2425
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 . && \
2530
cd /app/signingscript/docker.d && \
2631
bash build_msix_packaging.sh && \
2732
cp msix-packaging/.vs/bin/makemsix /usr/bin && \
2833
cp msix-packaging/.vs/lib/libmsix.so /usr/lib && \
2934
cd .. && \
3035
rm -rf msix-packaging && \
3136
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 \
37+
https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.25.1/apple-codesign-0.25.1-x86_64-unknown-linux-musl.tar.gz \
3338
| tar xvz -C /usr/bin --transform 's/.*\///g' --wildcards --no-anchored 'rcodesign' && \
3439
chmod +x /usr/bin/rcodesign
3540

@@ -40,6 +45,7 @@ WORKDIR /app
4045
# Install signingscript + configloader + widevine
4146
RUN python -m venv /app \
4247
&& cd signingscript \
48+
&& /app/bin/pip install /app/scriptworker_client \
4349
&& /app/bin/pip install -r requirements/base.txt \
4450
&& /app/bin/pip install . \
4551
&& python -m venv /app/configloader_venv \
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
- "credentials": {"$eval": "APPLE_SIGNING_CREDENTIALS"}
14+
"password": {"$eval": "APPLE_SIGNING_CREDS_PASSWORD"}
15+
'ENV != "prod" && scope_prefix':
16+
'${scope_prefix[0]}cert:dep-apple-signing':
17+
- "credentials": {"$eval": "APPLE_SIGNING_DEP_CREDENTIALS"}
18+
"password": {"$eval": "APPLE_SIGNING_DEP_CREDS_PASSWORD"}

signingscript/docker.d/init_worker.sh

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export HFSPLUS_PATH=$APP_DIR/signingscript/files/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/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)

signingscript/src/signingscript/script.py

+45-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from dataclasses import asdict
1010

1111
from signingscript.task import build_filelist_dict, sign, task_signing_formats, task_cert_type
12-
from signingscript.utils import copy_to_dir, load_apple_notarization_configs, load_autograph_configs
12+
from signingscript.utils import copy_to_dir, load_apple_notarization_configs, load_apple_signing_configs, load_autograph_configs, unlink
1313
from signingscript.exceptions import SigningScriptError
1414

1515
log = logging.getLogger(__name__)
@@ -40,6 +40,11 @@ async def async_main(context):
4040
raise Exception("Apple notarization is enabled but apple_notarization_configs is not defined")
4141
setup_apple_notarization_credentials(context)
4242

43+
if "apple_hardened_signing" in all_signing_formats:
44+
if not context.config.get("apple_signing_configs", False):
45+
raise Exception("Apple signing is enabled but apple_signing_configs is not defined")
46+
setup_apple_signing_credentials(context)
47+
4348
context.session = session
4449
context.autograph_configs = load_autograph_configs(context.config["autograph_configs"])
4550

@@ -101,17 +106,54 @@ def setup_apple_notarization_credentials(context):
101106

102107
context.apple_credentials_path = os.path.join(
103108
os.path.dirname(context.config["apple_notarization_configs"]),
104-
'apple_api_key.json',
109+
"apple_api_key.json",
105110
)
106111
if os.path.exists(context.apple_credentials_path):
107112
# TODO: If we have different api keys for each product, this needs to overwrite every task:
108113
return
109114
# Convert dataclass to dict so json module can read it
110115
credential = asdict(scope_credentials[0])
111-
with open(context.apple_credentials_path, 'wb') as credfile:
116+
with open(context.apple_credentials_path, "wb") as credfile:
112117
credfile.write(json.dumps(credential).encode("ascii"))
113118

114119

120+
def setup_apple_signing_credentials(context):
121+
"""Writes the signing p12 file and password to a file
122+
123+
Adds properties to context: apple_credentials_path + apple_credentials_pass_path
124+
125+
Args:
126+
context: Running task Context
127+
"""
128+
cert_type = task_cert_type(context)
129+
130+
apple_signing_configs = load_apple_signing_configs(context.config["apple_signing_configs"])
131+
if cert_type not in apple_signing_configs:
132+
raise SigningScriptError("Credentials not found for scope: %s" % cert_type)
133+
scope_credentials = apple_signing_configs.get(cert_type)
134+
if len(scope_credentials) != 1:
135+
raise SigningScriptError("There should only be 1 scope credential, %s found." % len(scope_credentials))
136+
137+
context.apple_signing_creds_path = os.path.join(
138+
os.path.dirname(context.config["apple_signing_configs"]),
139+
"apple_signing_creds.p12",
140+
)
141+
unlink(context.apple_signing_creds_path)
142+
context.apple_signing_creds_pass_path = os.path.join(
143+
os.path.dirname(context.config["apple_signing_configs"]),
144+
"apple_signing_creds_pass.passwd",
145+
)
146+
unlink(context.apple_signing_creds_pass_path)
147+
148+
# Convert dataclass to dict so json module can read it
149+
creds_config = asdict(scope_credentials[0])
150+
with open(context.apple_credentials_path, "wb") as credfile:
151+
credfile.write(json.dumps(creds_config["credentials"]).encode("ascii"))
152+
153+
with open(context.apple_signing_creds_pass_path, "wb") as passfile:
154+
passfile.write(json.dumps(creds_config["password"]).encode("ascii"))
155+
156+
115157
def main():
116158
"""Start signing script."""
117159
mohawk_log = logging.getLogger("mohawk")

0 commit comments

Comments
 (0)