Skip to content

Commit

Permalink
feat: Mac hardened signing on signingscript
Browse files Browse the repository at this point in the history
  • Loading branch information
hneiva committed Dec 7, 2023
1 parent ab97253 commit a1e4a4d
Show file tree
Hide file tree
Showing 25 changed files with 544 additions and 51 deletions.
23 changes: 12 additions & 11 deletions signingscript/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ RUN groupadd --gid 10001 app && \

# Copy only required folders
COPY ["signingscript", "/app/signingscript/"]
COPY ["scriptworker_client", "/app/scriptworker_client/"]
COPY ["configloader", "/app/configloader/"]
COPY ["docker.d", "/app/docker.d/"]
COPY ["vendored", "/app/vendored/"]
Expand All @@ -19,19 +20,18 @@ COPY ["vendored", "/app/vendored/"]
COPY ["version.jso[n]", "/app/"]

# Change owner of /app to app:app
# Build and install libdmg_hfsplus
# Install msix
# Install rcodesign
RUN chown -R app:app /app && \
cd /app/signingscript/docker.d && \
bash build_msix_packaging.sh && \
cp msix-packaging/.vs/bin/makemsix /usr/bin && \
cp msix-packaging/.vs/lib/libmsix.so /usr/lib && \
cd .. && \
rm -rf msix-packaging && \
wget -qO- \
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 \
| tar xvz -C /usr/bin --transform 's/.*\///g' --wildcards --no-anchored 'rcodesign' && \
chmod +x /usr/bin/rcodesign
RUN chown -R app:app /app \
&& cd /app/scriptworker_client \
&& pip install /app/scriptworker_client \
&& pip install -r requirements/base.txt \
&& pip install . \
&& cd /app/signingscript/docker.d \
&& bash build_libdmg_hfsplus.sh /usr/bin \
&& bash build_rcodesign.sh /usr/bin \
&& bash build_msix_packaging.sh

# Set user and workdir
USER app
Expand All @@ -40,6 +40,7 @@ WORKDIR /app
# Install signingscript + configloader + widevine
RUN python -m venv /app \
&& cd signingscript \
&& /app/bin/pip install /app/scriptworker_client \
&& /app/bin/pip install -r requirements/base.txt \
&& /app/bin/pip install . \
&& python -m venv /app/configloader_venv \
Expand Down
24 changes: 24 additions & 0 deletions signingscript/docker.d/apple_signing_creds.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
$let:
scope_prefix:
$match:
'COT_PRODUCT == "firefox"': 'project:releng:signing:'
'COT_PRODUCT == "thunderbird"': 'project:comm:thunderbird:releng:signing:'
'COT_PRODUCT == "mozillavpn"': 'project:mozillavpn:releng:signing:'
'COT_PRODUCT == "adhoc"': 'project:adhoc:releng:signing:'
in:
$merge:
$match:
'ENV == "prod" && scope_prefix':
'${scope_prefix[0]}cert:release-signing':
- "app_credentials": {"$eval": "APPLE_APP_SIGNING_CREDENTIALS"}
"installer_credentials": {"$eval": "APPLE_INSTALLER_SIGNING_CREDENTIALS"}
"password": {"$eval": "APPLE_SIGNING_CREDS_PASSWORD"}
'${scope_prefix[0]}cert:nightly-signing':
- "app_credentials": {"$eval": "APPLE_APP_SIGNING_CREDENTIALS"}
"installer_credentials": {"$eval": "APPLE_INSTALLER_SIGNING_CREDENTIALS"}
"password": {"$eval": "APPLE_SIGNING_CREDS_PASSWORD"}
'ENV != "prod" && scope_prefix':
'${scope_prefix[0]}cert:dep-signing':
- "app_credentials": {"$eval": "APPLE_APP_SIGNING_DEP_CREDENTIALS"}
"installer_credentials": {"$eval": "APPLE_INSTALLER_SIGNING_DEP_CREDENTIALS"}
"password": {"$eval": "APPLE_SIGNING_DEP_CREDS_PASSWORD"}
33 changes: 33 additions & 0 deletions signingscript/docker.d/build_libdmg_hfsplus.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/bin/bash
set -x -e -v

# This script is for building libdmg-hfsplus to get the `dmg` and `hfsplus`
# tools for handling DMG archives on Linux.

DEST=$1
if [ -d "$DEST" ]; then
echo "Binaries will be installed to: $DEST"
else
echo "Destination directory doesn't exist!"
exit 1
fi

git clone --depth=1 --branch mozilla --single-branch https://github.com/mozilla/libdmg-hfsplus/ libdmg-hfsplus

pushd libdmg-hfsplus

# The openssl libraries in the sysroot cannot be linked in a PIE executable so we use -no-pie
cmake \
-DOPENSSL_USE_STATIC_LIBS=1 \
-DCMAKE_EXE_LINKER_FLAGS=-no-pie \
.

make VERBOSE=1 -j$(nproc)

# We only need the dmg and hfsplus tools.
strip dmg/dmg hfs/hfsplus
cp dmg/dmg hfs/hfsplus "$DEST"

popd
rm -rf libdmg-hfsplus
echo "Done."
5 changes: 5 additions & 0 deletions signingscript/docker.d/build_msix_packaging.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ cd msix-packaging
./makelinux.sh --pack

cd ..

cp msix-packaging/.vs/bin/makemsix /usr/bin
cp msix-packaging/.vs/lib/libmsix.so /usr/lib

rm -rf msix-packaging
16 changes: 16 additions & 0 deletions signingscript/docker.d/build_rcodesign.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash
set -x -e -v

DEST=$1
if [ -d "$DEST" ]; then
echo "Binaries will be installed to: $DEST"
else
echo "Destination directory doesn't exist!"
exit 1
fi


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 \
| tar xvz -C "$DEST" --transform 's/.*\///g' --wildcards --no-anchored 'rcodesign'

chmod +x "${DEST}/rcodesign"
6 changes: 4 additions & 2 deletions signingscript/docker.d/init_worker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ test_var_set 'PROJECT_NAME'
test_var_set 'PUBLIC_IP'
test_var_set 'TEMPLATE_DIR'

export DMG_PATH=$APP_DIR/signingscript/files/dmg
export HFSPLUS_PATH=$APP_DIR/signingscript/files/hfsplus
export DMG_PATH=/usr/bin/dmg
export HFSPLUS_PATH=/usr/bin/hfsplus

export PASSWORDS_PATH=$CONFIG_DIR/passwords.json
export APPLE_NOTARIZATION_CREDS_PATH=$CONFIG_DIR/apple_notarization_creds.json
export APPLE_SIGNING_CONFIG_PATH=$CONFIG_DIR/apple_signing_config.json
export GPG_PUBKEY_PATH=$APP_DIR/signingscript/src/signingscript/data/gpg_pubkey_dep.asc
export WIDEVINE_CERT_PATH=$CONFIG_DIR/widevine.crt
export AUTHENTICODE_TIMESTAMP_STYLE=old
Expand Down Expand Up @@ -260,3 +261,4 @@ esac

$CONFIG_LOADER $TEMPLATE_DIR/passwords.yml $PASSWORDS_PATH
$CONFIG_LOADER $TEMPLATE_DIR/apple_notarization_creds.yml $APPLE_NOTARIZATION_CREDS_PATH
$CONFIG_LOADER $TEMPLATE_DIR/apple_signing_creds.yml $APPLE_SIGNING_CONFIG_PATH
1 change: 1 addition & 0 deletions signingscript/docker.d/worker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ verbose: { "$eval": "VERBOSE == 'true'" }
my_ip: { "$eval": "PUBLIC_IP" }
autograph_configs: { "$eval": "PASSWORDS_PATH" }
apple_notarization_configs: { "$eval": "APPLE_NOTARIZATION_CREDS_PATH" }
apple_signing_configs: { "$eval": "APPLE_SIGNING_CONFIG_PATH" }
taskcluster_scope_prefixes:
$flatten:
$match:
Expand Down
3 changes: 0 additions & 3 deletions signingscript/files/README

This file was deleted.

Binary file removed signingscript/files/dmg
Binary file not shown.
Binary file removed signingscript/files/hfsplus
Binary file not shown.
3 changes: 2 additions & 1 deletion signingscript/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
with open(os.path.join(os.path.abspath(os.path.dirname(__file__)), "version.txt")) as f:
version = f.read().rstrip()

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

setup(
name="signingscript",
Expand Down
42 changes: 42 additions & 0 deletions signingscript/src/signingscript/apple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import logging
import os
from shutil import copy2

from signingscript.exceptions import SigningScriptError

log = logging.getLogger(__name__)


PROVISIONING_PROFILE_FILENAMES = {
"firefox": "orgmozillafirefox.provisionprofile",
"devedition": "orgmozillafirefoxdeveloperedition.provisionprofile",
"nightly": "orgmozillanightly.provisionprofile",
}


def copy_provisioning_profiles(bundlepath, configs):
"""Copy provisioning profiles inside bundle
Args:
bundlepath (str): The absolute path to the app bundle
configs (list): The list of configs with schema [{"profile_name": str, "target_path": str}]
"""
for cfg in configs:
profile_name = cfg.get("profile_name")
target_path = cfg.get("target_path")
if not profile_name or not target_path:
raise SigningScriptError(f"profile_name and target_path are required. Got: {cfg}")

if profile_name not in PROVISIONING_PROFILE_FILENAMES.values():
raise SigningScriptError(f"profile_name not allowed: {profile_name}")

profile_path = os.path.join(os.path.dirname(__file__), "data", profile_name)
if not os.path.exists(profile_path):
raise SigningScriptError(f"Provisioning profile not found: {profile_name}")

# Resolve absolute destination path
target_abs_path = os.path.join(bundlepath, target_path if target_path[0] != "/" else target_path[1:])
if os.path.exists(target_abs_path):
log.warning("Provisioning profile at {target_path} already exists, overriding.")

log.info(f"Copying {profile_name} to {target_abs_path}")
copy2(profile_path, target_abs_path)
Binary file not shown.
141 changes: 140 additions & 1 deletion signingscript/src/signingscript/rcodesign.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
#!/usr/bin/env python
"""Functions that interface with rcodesign"""
import asyncio
from collections import namedtuple
import logging
import os
import re
from glob import glob

from scriptworker_client.aio import download_file, raise_future_exceptions, retry_async
from scriptworker_client.exceptions import DownloadError
from signingscript.exceptions import SigningScriptError

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

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


def find_submission_id(logs):
"""Given notarization logs, find and return the submission id
Args:
Expand Down Expand Up @@ -128,6 +135,7 @@ async def rcodesign_check_result(logs):
raise RCodesignError("Notarization failed!")
return


async def rcodesign_staple(path):
"""Staples a given app
Args:
Expand All @@ -146,3 +154,134 @@ async def rcodesign_staple(path):
if exitcode > 0:
raise RCodesignError(f"Error stapling notarization. Exit code {exitcode}")
return


def _create_empty_entitlements_file(dest):
contents = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
</dict>
</plist>
""".lstrip()
with open(dest, "wt") as fd:
fd.writelines(contents)


async def _download_entitlements(hardened_sign_config, workdir):
"""Download entitlements listed in the hardened signing config
Args:
hardened_sign_config (list): hardened signing configs
workdir (str): current work directory where entitlements will be saved
Returns:
Map of url -> local file location
"""
empty_file = os.path.join(workdir, "0-empty.xml")
_create_empty_entitlements_file(empty_file)
# rcodesign requires us to specify an "empty" entitlements file
url_map = {None: empty_file}

# Unique urls to be downloaded
urls_to_download = set([i["entitlements"] for i in hardened_sign_config if "entitlements" in i])
# If nothing found, skip
if not urls_to_download:
log.warn("No entitlements urls provided! Skipping download.")
return url_map

futures = []
for index, url in enumerate(urls_to_download, start=1):
# Prefix filename with an index in case filenames are the same
filename = "{}-{}".format(index, url.split("/")[-1])
dest = os.path.join(workdir, filename)
url_map[url] = dest
log.info(f"Downloading resource: {filename} from {url}")
futures.append(
asyncio.ensure_future(
retry_async(
download_file,
retry_exceptions=(DownloadError, TimeoutError),
args=(url, dest),
attempts=5,
)
)
)
await raise_future_exceptions(futures)
return url_map


EntitlementEntry = namedtuple(
"EntitlementEntry",
["file", "entitlement", "runtime"],
)

def _get_entitlements_args(hardened_sign_config, path, entitlements_map):
"""Builds the list of entitlements based on files in path
Args:
hardened_sign_config (list): hardened signing configuration
path (str): path to app
"""
entries = []

for config in hardened_sign_config:
entitlement_path = entitlements_map.get(config.get("entitlements"))
for path_glob in config["globs"]:
separator = ""
if not path_glob.startswith("/"):
separator = "/"
# Join incoming glob with root of app path
full_path_glob = path + separator + path_glob
for binary_path in glob(full_path_glob, recursive=True):
# Get relative path
relative_path = os.path.relpath(binary_path, path)
# Append "<binary path>:<entitlement>" to list of args
entries.append(
EntitlementEntry(
file=relative_path,
entitlement=entitlement_path,
runtime=config.get("runtime"),
)
)

return entries


async def rcodesign_sign(workdir, path, creds_path, creds_pass_path, hardened_sign_config=[]):
"""Signs a given app
Args:
workdir (str): Path to work directory
path (str): Path to be signed
creds_path (str): Path to credentials file
creds_pass_path (str): Path to credentials password file
hardened_sign_config (list): Hardened signing configuration
Returns:
(Tuple) exit code, log lines
"""
# TODO: Validate and sanitize input
command = [
"rcodesign",
"sign",
"--code-signature-flags=runtime",
f"--p12-file={creds_path}",
f"--p12-password-file={creds_pass_path}",
]

entitlements_map = await _download_entitlements(hardened_sign_config, workdir)
file_entitlements = _get_entitlements_args(hardened_sign_config, path, entitlements_map)

def _scoped_arg(arg, basepath, value):
if basepath == ".":
return f"--{arg}={value}"
return f"--{arg}={basepath}:{value}"

for entry in file_entitlements:
if entry.runtime:
flags_arg = _scoped_arg("code-signature-flags", entry.file, "runtime")
command.append(flags_arg)
entitlement_arg = _scoped_arg("entitlements-xml-path", entry.file, entry.entitlement)
command.append(entitlement_arg)

command.append(path)
await _execute_command(command)
Loading

0 comments on commit a1e4a4d

Please sign in to comment.