Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Mac hardened signing on signingscript #872

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 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,26 +20,22 @@ 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/signingscript/docker.d \
&& bash build_libdmg_hfsplus.sh /usr/bin \
&& bash install_rcodesign.sh /usr/bin \
&& bash build_msix_packaging.sh

# Set user and workdir
USER app
WORKDIR /app

# Install signingscript + configloader + widevine
RUN python -m venv /app \
&& /app/bin/pip install /app/scriptworker_client \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: why are we installing scriptworker_client twice? (and its related requirements). If this is not necessary, please remove it from the above block.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant to remove it from the block above since it has no effect whatsoever (root user)

&& cd signingscript \
&& /app/bin/pip install -r requirements/base.txt \
&& /app/bin/pip install . \
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_pkcs12_bundle": {"$eval": "APPLE_APP_SIGNING_PKCS12"}
"installer_pkcs12_bundle": {"$eval": "APPLE_INSTALLER_SIGNING_PKCS12"}
"pkcs12_password": {"$eval": "APPLE_SIGNING_PKCS12_PASSWORD"}
'${scope_prefix[0]}cert:nightly-signing':
- "app_pkcs12_bundle": {"$eval": "APPLE_APP_SIGNING_PKCS12"}
"installer_pkcs12_bundle": {"$eval": "APPLE_INSTALLER_SIGNING_PKCS12"}
"pkcs12_password": {"$eval": "APPLE_SIGNING_PKCS12_PASSWORD"}
'ENV != "prod" && scope_prefix':
'${scope_prefix[0]}cert:dep-signing':
- "app_pkcs12_bundle": {"$eval": "APPLE_APP_SIGNING_DEP_PKCS12"}
"installer_pkcs12_bundle": {"$eval": "APPLE_INSTALLER_SIGNING_DEP_PKCS12"}
"pkcs12_password": {"$eval": "APPLE_SIGNING_DEP_PKCS12_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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: thank you for cleaning up the left I mess here :)

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
16 changes: 16 additions & 0 deletions signingscript/docker.d/install_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"
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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: thank you for updating/fixing this!


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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): this might be better off living in a config somewhere

"devedition": "orgmozillafirefoxdeveloperedition.provisionprofile",
"nightly": "orgmozillanightly.provisionprofile",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: Only one of these files exists. The others should either by added, or commented out for now. (We'll get error messages about profile_name not allowed instead of provisioning profile not found this way, which I think makes things more clear.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the missing files

}


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.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Do we know that overriding an existing provisioning profile is always the right thing to do? Are there even any cases where we expect one in a bundle already, or would that be indicative of a problem further up the chain?

Either way, thank you for logging this explicitly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under normal scenarios it shouldn't happen. But when testing things, this makes sure the profile is always "what it should be".


log.info(f"Copying {profile_name} to {target_abs_path}")
copy2(profile_path, target_abs_path)
Binary file not shown.
Binary file not shown.
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