Showing preview only (485K chars total). Download the full file or copy to clipboard to get everything.
Repository: apperian/isign
Branch: develop
Commit: e5c71c645638
Files: 191
Total size: 440.0 KB
Directory structure:
gitextract_wnigoa8g/
├── .gitignore
├── CONDUCT.md
├── INSTALL.sh
├── LICENSE.txt
├── MANIFEST.in
├── PKG-INFO
├── PREREQUISITES.rst
├── README.rst
├── apple/
│ ├── README.rst
│ ├── provisions.py
│ └── provisions.sh
├── bin/
│ ├── isign
│ ├── isign_export_creds.sh
│ ├── isign_guess_mobileprovision.sh
│ ├── make_seal
│ ├── multisign
│ └── pprint_codesig
├── dev/
│ ├── requirements.txt
│ └── setup.sh
├── docs/
│ ├── applecerts.rst
│ ├── codedirectory.rst
│ ├── credentials.rst
│ └── speed.rst
├── isign/
│ ├── __init__.py
│ ├── apple_credentials/
│ │ └── README.rst
│ ├── archive.py
│ ├── bundle.py
│ ├── code_resources.py
│ ├── code_resources_template.xml
│ ├── codesig.py
│ ├── der_encoder.py
│ ├── exceptions.py
│ ├── isign.py
│ ├── macho.py
│ ├── macho_cs.py
│ ├── makesig.py
│ ├── multisign.py
│ ├── signable.py
│ ├── signer.py
│ └── utils.py
├── jenkins.sh
├── run_tests.sh
├── setup.cfg
├── setup.py
├── tests/
│ ├── NotAnApp.ipa
│ ├── NotAnApp.txt
│ ├── NotAnAppDir/
│ │ └── README.md
│ ├── README.md
│ ├── Test.app/
│ │ ├── Assets.car
│ │ ├── Base.lproj/
│ │ │ ├── LaunchScreen.storyboardc/
│ │ │ │ ├── 01J-lp-oVM-view-Ze5-6b-2t3.nib
│ │ │ │ ├── Info.plist
│ │ │ │ └── UIViewController-01J-lp-oVM.nib
│ │ │ └── Main.storyboardc/
│ │ │ ├── 8rJ-Kc-sve-view-QS5-Rx-YEW.nib
│ │ │ ├── 9pv-A4-QxB-view-tsR-hK-woN.nib
│ │ │ ├── Info.plist
│ │ │ └── UITabBarController-49e-Tb-3d3.nib
│ │ ├── Info.plist
│ │ ├── PkgInfo
│ │ ├── _CodeSignature/
│ │ │ └── CodeResources
│ │ ├── build.sh
│ │ ├── isignTestApp
│ │ ├── isignTestApp.xcodeproj/
│ │ │ ├── project.pbxproj
│ │ │ ├── project.xcworkspace/
│ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ └── xcuserdata/
│ │ │ │ └── neilk.xcuserdatad/
│ │ │ │ └── UserInterfaceState.xcuserstate
│ │ │ └── xcuserdata/
│ │ │ └── neilk.xcuserdatad/
│ │ │ └── xcschemes/
│ │ │ ├── isignTestApp.xcscheme
│ │ │ └── xcschememanagement.plist
│ │ ├── isignTestAppTests/
│ │ │ ├── Info.plist
│ │ │ └── isignTestAppTests.swift
│ │ └── isignTestAppUITests/
│ │ ├── Info.plist
│ │ └── isignTestAppUITests.swift
│ ├── Test.app.codesig.construct.txt
│ ├── Test.ipa
│ ├── TestWithFrameworks.ipa
│ ├── Test_unsigned_fat.app/
│ │ ├── Assets.car
│ │ ├── Base.lproj/
│ │ │ ├── LaunchScreen.storyboardc/
│ │ │ │ ├── 01J-lp-oVM-view-Ze5-6b-2t3.nib
│ │ │ │ ├── Info.plist
│ │ │ │ └── UIViewController-01J-lp-oVM.nib
│ │ │ └── Main.storyboardc/
│ │ │ ├── 8rJ-Kc-sve-view-QS5-Rx-YEW.nib
│ │ │ ├── 9pv-A4-QxB-view-tsR-hK-woN.nib
│ │ │ ├── Info.plist
│ │ │ └── UITabBarController-49e-Tb-3d3.nib
│ │ ├── Info.plist
│ │ ├── PkgInfo
│ │ ├── build.sh
│ │ ├── isignTestApp
│ │ ├── isignTestApp.xcodeproj/
│ │ │ ├── project.pbxproj
│ │ │ ├── project.xcworkspace/
│ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ └── xcuserdata/
│ │ │ │ └── neilk.xcuserdatad/
│ │ │ │ └── UserInterfaceState.xcuserstate
│ │ │ └── xcuserdata/
│ │ │ └── neilk.xcuserdatad/
│ │ │ └── xcschemes/
│ │ │ ├── isignTestApp.xcscheme
│ │ │ └── xcschememanagement.plist
│ │ ├── isignTestAppTests/
│ │ │ ├── Info.plist
│ │ │ └── isignTestAppTests.swift
│ │ └── isignTestAppUITests/
│ │ ├── Info.plist
│ │ └── isignTestAppUITests.swift
│ ├── Test_unsigned_thin.app/
│ │ ├── Assets.car
│ │ ├── Base.lproj/
│ │ │ ├── LaunchScreen.storyboardc/
│ │ │ │ ├── 01J-lp-oVM-view-Ze5-6b-2t3.nib
│ │ │ │ ├── Info.plist
│ │ │ │ └── UIViewController-01J-lp-oVM.nib
│ │ │ └── Main.storyboardc/
│ │ │ ├── 8rJ-Kc-sve-view-QS5-Rx-YEW.nib
│ │ │ ├── 9pv-A4-QxB-view-tsR-hK-woN.nib
│ │ │ ├── Info.plist
│ │ │ └── UITabBarController-49e-Tb-3d3.nib
│ │ ├── Info.plist
│ │ ├── PkgInfo
│ │ ├── build.sh
│ │ ├── isignTestApp
│ │ ├── isignTestApp.xcodeproj/
│ │ │ ├── project.pbxproj
│ │ │ ├── project.xcworkspace/
│ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ └── xcuserdata/
│ │ │ │ └── neilk.xcuserdatad/
│ │ │ │ └── UserInterfaceState.xcuserstate
│ │ │ └── xcuserdata/
│ │ │ └── neilk.xcuserdatad/
│ │ │ └── xcschemes/
│ │ │ ├── isignTestApp.xcscheme
│ │ │ └── xcschememanagement.plist
│ │ ├── isignTestAppTests/
│ │ │ ├── Info.plist
│ │ │ └── isignTestAppTests.swift
│ │ └── isignTestAppUITests/
│ │ ├── Info.plist
│ │ └── isignTestAppUITests.swift
│ ├── bad_openssl
│ ├── credentials/
│ │ ├── README.rst
│ │ ├── makeFakePprof.sh
│ │ ├── test.cert.pem
│ │ ├── test.key.pem
│ │ ├── test.mobileprovision
│ │ └── test.mobileprovision.plist
│ ├── credentials_std_names/
│ │ ├── README.rst
│ │ ├── certificate.pem
│ │ └── isign.mobileprovision
│ ├── credentials_std_names_2/
│ │ ├── README.rst
│ │ ├── certificate.pem
│ │ ├── isign.mobileprovision
│ │ └── key.pem
│ ├── generate_codesig_construct_txt.py
│ ├── isignTestApp/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── build.sh
│ │ ├── exportOptions.plist
│ │ ├── isignTestApp/
│ │ │ ├── AppDelegate.swift
│ │ │ ├── Assets.xcassets/
│ │ │ │ ├── AppIcon.appiconset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── first.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ └── second.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Base.lproj/
│ │ │ │ ├── LaunchScreen.storyboard
│ │ │ │ └── Main.storyboard
│ │ │ ├── FirstViewController.swift
│ │ │ ├── Info.plist
│ │ │ └── SecondViewController.swift
│ │ ├── isignTestApp.xcodeproj/
│ │ │ ├── project.pbxproj
│ │ │ ├── project.xcworkspace/
│ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ └── xcuserdata/
│ │ │ │ └── neilk.xcuserdatad/
│ │ │ │ └── UserInterfaceState.xcuserstate
│ │ │ └── xcuserdata/
│ │ │ └── neilk.xcuserdatad/
│ │ │ └── xcschemes/
│ │ │ ├── isignTestApp.xcscheme
│ │ │ └── xcschememanagement.plist
│ │ ├── isignTestAppTests/
│ │ │ ├── Info.plist
│ │ │ └── isignTestAppTests.swift
│ │ └── isignTestAppUITests/
│ │ ├── Info.plist
│ │ └── isignTestAppUITests.swift
│ ├── isignTestAppWithFrameworks/
│ │ ├── .gitignore
│ │ ├── Podfile
│ │ ├── README.md
│ │ ├── build.sh
│ │ ├── exportOptions.plist
│ │ ├── isignTestApp/
│ │ │ ├── AppDelegate.swift
│ │ │ ├── Assets.xcassets/
│ │ │ │ ├── AppIcon.appiconset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── first.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ └── second.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Base.lproj/
│ │ │ │ ├── LaunchScreen.storyboard
│ │ │ │ └── Main.storyboard
│ │ │ ├── FirstViewController.swift
│ │ │ ├── Info.plist
│ │ │ └── SecondViewController.swift
│ │ ├── isignTestApp.xcodeproj/
│ │ │ ├── project.pbxproj
│ │ │ ├── project.xcworkspace/
│ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ └── xcuserdata/
│ │ │ │ └── neilk.xcuserdatad/
│ │ │ │ └── UserInterfaceState.xcuserstate
│ │ │ └── xcuserdata/
│ │ │ └── neilk.xcuserdatad/
│ │ │ └── xcschemes/
│ │ │ ├── isignTestApp.xcscheme
│ │ │ └── xcschememanagement.plist
│ │ ├── isignTestApp.xcworkspace/
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcuserdata/
│ │ │ └── neilk.xcuserdatad/
│ │ │ └── UserInterfaceState.xcuserstate
│ │ ├── isignTestAppTests/
│ │ │ ├── Info.plist
│ │ │ └── isignTestAppTests.swift
│ │ └── isignTestAppUITests/
│ │ ├── Info.plist
│ │ └── isignTestAppUITests.swift
│ ├── isign_base_test.py
│ ├── monitor_temp_file.py
│ ├── sample-entitlements.plist
│ ├── test_archive.py
│ ├── test_creds_dir.py
│ ├── test_entitlements.py
│ ├── test_helpers.py
│ ├── test_multisign.py
│ ├── test_parsing.py
│ ├── test_public_interface.py
│ ├── test_sig.py
│ ├── test_versioning.py
│ └── test_versus_apple.py
└── version.sh
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# generic
*~
*#
*._*
*.log
*.log.*
*.pid
# apple
.DS_Store
*~.nib
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
*.pot
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Development
.*.sw[po]
*.sublime-project
*.sublime-workspace
.project
.pydevproject
.ropeproject
*.sass-cache
.aws.env
.codeintel
.coverage
.noseids
.semantic.cache
.settings
.idea
.realsync
*.tm_build_errors
tmtags
*.iml
# iresign version
isign/version.json
# iresign temp files
Entitlements.plist
stage
out.app
out.ipa
# all credentials
*.pem
*.p12
*.mobileprovision
# except these
!apple_credentials/applecerts.pem
!tests/credentials/test.key.pem
!tests/credentials/test.cert.pem
!tests/credentials/test.mobileprovision
!tests/credentials_std_names/certificate.pem
!tests/credentials_std_names/isign.mobileprovision
!tests/credentials_std_names_2/key.pem
!tests/credentials_std_names_2/certificate.pem
!tests/credentials_std_names_2/isign.mobileprovision
================================================
FILE: CONDUCT.md
================================================
## Code of Conduct
### What is this code of conduct for?
`isign` is a piece of technology, but **the core of the `isign` community is the people in it**. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, gender identity and expression, sexual orientation, ability, physical appearance, body size, race, age, socioeconomic status, religion (or lack thereof), or other marginalized aspect of comunity members. We expect all members of the `isign` community to abide by this Code of Conduct whenever interacting in `isign` venues (pull requests, GitHub issues, 1-1 or group chat, meetups, conferences, etc...)
### Examples of inappropriate behavior
Because we come from a variety of backgrounds, we don't want to assume that everyone has the same assumptions about what is and isn't appropriate. Here are some examples of inappropriate behavior that are incompatible with our community's ethos:
* Spamming, trolling, intentionally disrupting conversations, or irrelevant solicitation or advertisement
* Making demeaning or discriminatory comments
* Making negative assumptions about someone's background, abilities, or intentions
* Harassing or stalking individuals (online or in person)
* Giving someone unwelcome sexual attention or making unwelcome physical contact (in the case of an IRL event)
* Sharing sexual images or using sexually explicit language
In general: treat others how you would like to be treated, were you in their place. Don't be a jerk. _Do_ ask questions. _Do_ keep conflicts productively focused on technical issues. _Do_ think before you speak; remember that what is perceived as a funny witticism in your group of friends might be hurtful or reinforce hurtful stereotypes in the context of our diverse online community. _Do_ remember that we are all people, not robots, and all equally deserving of sensitivity and respect. (If and when robots join our community, let's treat them with respect too!)
### What will organizers do about inappropriate behavior?
If we notice you doing or saying something inappropriate, an organizer will explain why it's inappropriate and ask you to stop. We won't demonize or vilify you. But please do stop the inappropriate behavior so we can get back to writing and discussing code in a safe environment. If you have philosophical disagreements about what's actually inappropriate, please take them to a separate public or private conversation with an `isign` maintainer so we don't turn pull requests into an ethics debate.
If you keep doing unacceptable things, we'll likely ban you, report you to GitHub, or take other appropriate action.
### What if I see or am subject to what feels like inappropriate behavior?
Let us know! Please notify a community organizer as soon as possible. Full contact information is listed in the [Contact Info](#contact-info) section of this document. All communications will be kept strictly confidential, unless otherwise required by law. No issue will be considered too inconsequential or unimportant for us to have a conversation about.
### Contact Info
If you need to report an incident, please contact any of the following organizers directly:
* Neil Kandalgaonkar [email](mailto:neilk@neilk.net) [twitter](https://twitter.com/flipzagging/)
* Jonathan Lipps [email](mailto:jlipps@saucelabs.com) [twitter](https://twitter.com/jlipps)
### Credit, License, and Attribution
This Code of Conduct is distributed under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/).
It's inspired among other things by:
* [Citizen Code of Conduct](http://citizencodeofconduct.org/)
* [npmjs](https://www.npmjs.com/policies/conduct)
* [Geek Feminism](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy)
* [Ashe Dryden](http://www.ashedryden.com/blog/codes-of-conduct-101-faq)
* [Model View Culture](https://modelviewculture.com/issues/events)
* [Open Source & Feelings](http://osfeels.com/conduct).
================================================
FILE: INSTALL.sh
================================================
#!/bin/bash
set -e
REQUIRED_OPENSSL_VERSION="1.0.1"
REQUIRED_PIP_VERSION="7.0.0"
BREW_USER="unknown"
DEVELOP=false
if [[ "$1" == 'develop' ]]; then
DEVELOP=true
fi
abort_install() {
warn "Aborting the install. If you want to install prerequisites"
warn "manually, see PREREQUISITES.rst."
exit 0;
}
warn() {
echo "$@" 1>&2;
}
trim() {
# has the side effect of trimming whitespace
echo "$@" | xargs
}
get_ok_to_upgrade() {
warn "This script may install or upgrade the following software:"
warn " - brew";
warn " - openssl";
warn " - libffi"
warn " - python and pip";
warn " - libimobiledevice and related utilities";
read -p "Okay to continue? [Y/n]" -n 1 -r
warn " "
warn " "
if [[ $REPLY =~ ^[Nn]$ ]]; then
abort_install
fi
}
# given arguments like 1.0.2e, 3, returns "2"
get_version_part() {
local version=$1
local part=$2
echo $version | cut -f $part -d '.' | sed 's/[^0-9]//g'
}
check_version() {
local required=$1;
local given=$2;
local i
for i in 1 2 3; do
local required_part=$(get_version_part $required $i);
local given_part=$(get_version_part $given $i);
if [[ $given_part -lt $required_part ]]; then
return 1;
fi
done;
return 0;
}
exists() {
# warn "checking for existence of $1"
which $1 > /dev/null
}
openssl_version_ok() {
local openssl_path=$1
warn "Trying to check if $openssl_path openssl version is okay..."
if [[ -e $openssl_path ]]; then
local openssl_version=$($openssl_path version | cut -f 2 -d ' ');
warn "Found $openssl_path version $openssl_version."
if check_version $REQUIRED_OPENSSL_VERSION $openssl_version; then
warn "$openssl_path version looks okay."
return 0;
fi
fi
warn "$openssl_path version was not okay!"
return 1;
}
setup_brew() {
if exists brew; then
warn "You seem to have brew."
else
# This is what https://brew.sh recommends...
# Installing straight from a URL? What could go wrong?
local homebrew_install_url="https://raw.githubusercontent.com/Homebrew/install/master/install"
warn "We need to install brew..."
warn "Okay to install via ruby script at this URL?"
warn " $homebrew_install_url";
warn " "
read -p "Okay to continue? [Y/n]" -n 1 -r
warn " "
warn " "
if [[ $REPLY =~ ^[Nn]$ ]]; then
abort_install
fi
ruby -e "$(curl -fsSL $homebrew_install_url)";
fi
# which user owns the Cellar? We'll need this to install things...
BREW_USER=$(stat -f '%Su' `brew --cellar`)
warn "brew's stuff seems to be owned by $BREW_USER..."
return 0;
}
# Commands that write to brew's Cellar need to not be root
brew_write() {
local command=$1
local package=$2
if [[ $EUID -eq 0 ]]; then
sudo -u $BREW_USER brew $command $package
else
brew $command $package
fi
}
# some brew packages have this in machine readable form
# but not all :(
brew_get_flags() {
local package=$1
local flags=$2
brew info $package | grep $flags | awk '{print $2}'
}
# check if a program is managed by brew
# it might be a symlink like /usr/local/bin/openssl, pointing to somewhere in the Cellar
is_brew_program() {
readlink `which $1` | grep `brew --prefix` >/dev/null;
}
mac_setup_openssl() {
# warn "start mac_setup_openssl"
# if the currently installed openssl doesn't meet our requirements,
# install or upgrade with brew
local openssl_path=$(which openssl)
if [[ -n $openssl_path ]]; then
if ! openssl_version_ok $openssl_path; then
brew_setup_openssl
fi
fi
# At this point, the openssl version is okay. It may or may not
# be a brew installed program. If it is, set up some compilation
# library paths. We append to $LDFLAGS and $CPPFLAGS because some other
# things put flags in there too.
if is_brew_program openssl; then
openssl_ldflags=$(brew_get_flags openssl LDFLAGS)
export LDFLAGS=$(trim "$LDFLAGS $openssl_ldflags")
openssl_cppflags=$(brew_get_flags openssl CPPFLAGS)
export CPPFLAGS=$(trim "$CPPFLAGS $openssl_cppflags")
# Some stackoverflow answers use this too?
export CFLAGS=$CPPFLAGS
fi
return 0;
}
brew_setup_openssl() {
# So now we know there either wasn't an openssl, or it wasn't
# a version good enough. Time for brew!
# is the brew openssl installed? if not, install
brew_openssl_path=$(brew list openssl | grep -e '/openssl$')
# warn "brew openssl path = $brew_openssl_path"
if [[ -z $brew_openssl_path ]]; then
warn "Installing openssl..."
brew_write install openssl
brew_openssl_path=$(brew list openssl | grep -e '/openssl$')
fi
# warn "brew openssl path = $brew_openssl_path"
# is the brew openssl the right version? if not, upgrade
if ! openssl_version_ok $brew_openssl_path; then
warn "Upgrading openssl..."
brew_write upgrade openssl
brew_openssl_path=$(brew list openssl | grep -e '/openssl$')
fi
# warn "okay by now $brew_openssl_path should be an acceptable openssl"
# for various reasons, brew will refuse to simultaneously
# be root and link the brew openssl binary somewhere useful.
# So we do it manually. We assume that the brew --prefix will be
# in the user's $PATH, and will take precedence over system openssl.
brew_link_path=$(brew --prefix)/bin/openssl
# warn "brew link path is $brew_link_path"
if [[ -e brew_link_path ]]; then
# warn "removing existing link"
rm brew_link_path;
fi
warn "Linking $brew_openssl_path $brew_link_path."
ln -s $brew_openssl_path $brew_link_path
# let's see if we succeeded: the openssl in our path should now be ready!
new_openssl_path=$(which openssl)
# warn "new path is $new_openssl_path"
if ! openssl_version_ok $new_openssl_path; then
warn "We tried to install an openssl >$MINIMUM_OPENSSL_VERSION, but we failed."
warn "Check if $brew_link_path is in your \$PATH ($PATH)."
return 1
fi
# warn "success, finally"
return 0;
}
mac_setup_libffi() {
warn "Checking for libffi..."
if ! brew list libffi 2>/dev/null >/dev/null; then
warn "Nope, no libffi. Installing..."
brew_write install libffi
fi
# set up some compilation library paths (we'll need it later...)
local libffi_ldflags=$(brew_get_flags libffi LDFLAGS)
export LDFLAGS=$(trim "$LDFLAGS $libffi_ldflags")
}
mac_setup_python() {
if exists pip; then
pip_version=$(pip --version | awk '{ print $2 }')
if check_version $REQUIRED_PIP_VERSION $pip_version; then
warn "pip version $pip_version looks okay."
return 0;
fi
fi
brew_write install python
}
mac_setup_libimobiledevice() {
if exists ideviceinstaller; then
return 0;
fi
brew_write install libimobiledevice
}
mac_setup() {
get_ok_to_upgrade
setup_brew
mac_setup_openssl
mac_setup_libffi
mac_setup_python
mac_setup_libimobiledevice
}
linux_setup() {
#apt-get install ideviceinstaller
#apt-get install libimobiledevice-utils
echo "Skipping apt-get installs, we use yum"
}
unamestr=`uname`
if [[ "$unamestr" == 'Darwin' ]]; then
mac_setup
elif [[ "$unamestr" == 'Linux' ]]; then
linux_setup
else
warn "Sorry, I don't know how to install on $unamestr.";
exit 1;
fi;
echo "--- Flags ---"
echo "LDFLAGS=$LDFLAGS"
echo "CPPFLAGS=$CPPFLAGS"
echo "CFLAGS=$CFLAGS"
echo
if [[ "$DEVELOP" == true ]]; then
python setup.py develop
else
python setup.py install
fi
================================================
FILE: LICENSE.txt
================================================
Copyright 2015 Sauce Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: MANIFEST.in
================================================
include isign/version.json
include isign/apple_credentials/applecerts.pem
include isign/code_resources_template.xml
================================================
FILE: PKG-INFO
================================================
Name: isign
Version: 1.6.16
================================================
FILE: PREREQUISITES.rst
================================================
Prerequisites
=============
For Linux or Mac platforms, the ``INSTALL.sh`` script should take care of
everything you need.
If you don't want to run that script on the machine where you want to re-sign apps,
this will explain what you need.
First, on the machine where you're going to re-sign apps, ensure
`openssl <https://www.openssl.org>`__ is at version 1.0.1 or better, like
this:
.. code::
$ openssl version
OpenSSL 1.0.1 14 Mar 2012
If that looks okay, you can probably install. If not:
.. _Linux:
Linux
~~~~~
You can probably easily update this with your package manager, such as
``apt-get upgrade openssl``.
.. _Mac OS X:
Mac OS X
~~~~~~~~
With OS X 10.11 "El Capitan", Apple stopped shipping some programs, libraries, and
headers that we'll need. You can use `homebrew <http://brew.sh>`__ to install them:
.. code::
$ brew install openssl libffi
You will also have to put ``brew``'s openssl into your path somehow, probably like this:
.. code::
$ brew list openssl
...
/usr/local/Cellar/openssl/1.0.2e/bin/openssl <-- you want this
...
$ ln -s /usr/local/Cellar/openssl/1.0.2e/bin/openssl /usr/local/bin/openssl
If you really don't want to alter the default ``openssl``, you can put the path to brew's
``openssl`` in an environment variable, ``$OPENSSL``, e.g.
.. code::
$ export OPENSSL=/usr/local/Cellar/openssl/1.0.2e/bin/openssl
If ``isign`` sees that, it will use that for its ``openssl`` instead.
Anyway, no matter how you install the binary, to complete the installation of ``isign``
you need to add some library paths to your environment. The procedure will
look something like this.
.. code::
$ brew info openssl
...
build variables:
LDFLAGS: -L/usr/local/opt/openssl/lib
CPPFLAGS: -I/usr/local/opt/openssl/include
$ brew info libffi
...
build variables:
LDFLAGS: -L/usr/local/opt/libffi/lib
Then, take the flags from above, and put them into appropriate environment
variables:
.. code::
$ export LDFLAGS="-L/usr/local/opt/openssl/lib -L/usr/local/opt/libffi/lib"
$ export CPPFLAGS="-I/usr/local/opt/openssl/include"
Finally, be aware that the ``python`` that ships with Mac OS X doesn't have the package
manager ``pip``. You can probably use ``easy_install`` instead of ``pip``. Or, you can get a more
up-to-date python with ``brew install python``.
Now you're probably ready to install ``isign``. A simple ``pip install isign`` should succeed.
================================================
FILE: README.rst
================================================
isign
=====
A tool and library to re-sign iOS applications, without proprietary Apple software.
For example, an iOS app in development would probably only run on the developer's iPhone.
``isign`` can alter the app so that it can run on another developer's iPhone.
Apple tools already exist to do this. But with ``isign``, now you can do this on operating
systems like Linux.
Table of contents
-----------------
- `Installing`_
- `How to get started`_
- `How to use isign`_
- `isign command line arguments`_
- `Contributing`_
- `More documentation`_
- `Authors`_
.. _Installing:
Installing
----------
Linux
~~~~~
The latest version of ``isign`` can be installed via `PyPi <https://pypi.python.org/pypi/isign/>`__:
.. code::
$ pip install isign
Mac OS X
~~~~~~~~
On Mac OS X, there are a lot of prerequisites, so the ``pip`` method probably won't work.
The easiest method is to use ``git`` to clone the `source code repository <https://github.com/saucelabs/isign>`__ and
run the install script:
.. code::
$ git clone https://github.com/saucelabs/isign.git
$ cd isign
$ sudo ./INSTALL.sh
.. _How to get started:
How to get started
------------------
All the libraries and tools that ``isign`` needs to run will work on both Linux
and Mac OS X. However, you will need a Mac to export your Apple developer
credentials.
If you're like most iOS developers, credentials are confusing -- if so check out
the `documentation on credentials <https://github.com/saucelabs/isign/blob/master/docs/credentials.rst>`__ on Github.
You should have a key and certificate in
`Keychain Access <https://en.wikipedia.org/wiki/Keychain_(software)>`__,
and a provisioning profile associated with that certificate, that you
can use to sign iOS apps for one or more of your own iOS devices.
In Keychain Access, open the *Certificates*. Find the certificate you use to sign apps.
Right click on it and export the key as a ``.p12`` file, let's say ``Certificates.p12``. If Keychain
asks you for a password to protect this file, just leave it blank.
Next, let's extract the key and certificate you need, into a standard PEM format.
.. code::
$ isign_export_creds.sh ~/Certificates.p12
If you get prompted for a password, just press ``Return``.
By default, ``isign_export_creds.sh`` will put these files into ``~/.isign``, which is
the standard place to put ``isign`` configuration files.
Finally, you need a provisioning profile from the Apple Developer Portal that uses
the same certificate. If you've never dealt with this, the provisioning profile is
what tells the phone that you Apple has okayed you installing apps onto this particular phone.
If you develop with XCode, you might have a provisioning profile already.
On the Mac where you develop with XCode, try running the ``isign_guess_mobileprovision.sh`` script.
If you typically have only a few provisioning profiles and install on one phone, it might find it.
Anyway, once you have a ``.mobileprovision`` file, move it to ``~/.isign/isign.mobileprovision``.
The end result should look like this:
.. code::
$ ls -l ~/.isign
-r--r--r-- 1 alice staff 2377 Sep 4 14:17 certificate.pem
-r--r--r-- 1 alice staff 9770 Nov 23 13:30 isign.mobileprovision
-r-------- 1 alice staff 1846 Sep 4 14:17 key.pem
And now you're ready to start re-signing apps!
.. _How to use isign:
How to use isign
----------------
If you've installed all the files in the proper locations above, then ``isign`` can be now invoked
on any iOS ``.app`` directory, or ``.ipa`` archive, or ``.app.zip`` zipped directory. For example:
.. code::
$ isign -o resigned.ipa my.ipa
archived Ipa to /home/alice/resigned.ipa
You can also call it from Python:
.. code:: python
from isign import isign
isign.resign("my.ipa", output_path="resigned.ipa")
.. _isign command line arguments:
isign command line arguments
----------------------------
.. code::
# Resigning by specifying all credentials, input file, and output file
$ isign -c /path/to/mycert.pem -k ~/mykey.pem -p path/to/my.mobileprovision \
-o resigned.ipa original.ipa
# Resigning, with credentials under default filenames in ~/.isign - less to type!
$ isign -o resigned.ipa original.ipa
# Modify Info.plist properties in resigned app
$ isign -i CFBundleIdentifier=com.example.myapp,CFBundleName=MyApp -o resigned.ipa original.ipa
# Display Info.plist properties from an app as JSON
$ isign -d my.ipa
# Get help
$ isign -h
**-a <path>, --apple-cert <path>**
Path to Apple certificate in PEM format. This is already included in the library, so you will likely
never need it. In the event that the certificates need to be changed, See the `Apple Certificate documentation <docs/applecerts.rst>`__.
**-c <path>, --certificate <path>**
Path to your certificate in PEM format. Defaults to ``$HOME/.isign/certificate.pem``.
**-d, --display**
For the application path, display the information property list (Info.plist) as JSON.
**-h, --help**
Show a help message and exit.
**-i, --info**
While resigning, add or update info in the application's information property list (Info.plist).
Takes a comma-separated list of key=value pairs, such as
``CFBundleIdentifier=com.example.app,CFBundleName=ExampleApp``. Use with caution!
See Apple documentation for `valid Info.plist keys <https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Introduction/Introduction.html>`_.
**-k <path>, --key <path>**
Path to your private key in PEM format. Defaults to ``$HOME/.isign/key.pem``.
**-n <directory>, --credentials <directory>**
Equivalent to:
.. code::
-k <directory>/key.pem
-c <directory>/certificate.pem
-p <directory>/isign.mobileprovision
**-o <path>, --output <path>**
Path to write the re-signed application. Defaults to ``out`` in your current working directory.
**-p <path>, --provisioning-profile <path>**
Path to your provisioning profile. This should be associated with your certificate. Defaults to
``$HOME/.isign/isign.mobileprovision``.
**-v, --verbose**
More verbose logs will be printed to STDERR.
**Application path**
The app to be resigned is specified on the command line after other arguments. The application path is
typically an IPA, but can also be a ``.app`` directory or even a zipped ``.app`` directory. When
resigning, ``isign`` will always create an archive of the same type as the original.
.. _Contributing:
Contributing
------------
Sauce Labs open source projects have a `Code of Conduct <CONDUCT.md>`__. In short, we try to respect each other,
listen, and be helpful.
Development happens on `our Github repository <https://github.com/saucelabs/isign>`__. File an issue, or fork the code!
You'll probably want to create some kind of python virtualenv, so you don't have to touch your system python or its
libraries. `virtualenvwrapper <https://virtualenvwrapper.readthedocs.org/en/latest/>`__ is a good tool for this.
Then, just do the following:
.. code::
$ git clone https://github.com/saucelabs/isign.git
$ cd isign
$ dev/setup.sh
$ ./run_tests.sh
If the tests don't pass please `file an issue <https://github.com/saucelabs/isign/issues>`__. Please keep the tests up to date as you develop.
Note: some tests require Apple's
`codesign <https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/codesign.1.html>`__
to run, so they are skipped unless you run them on a Macintosh computer with developer tools.
Okay, if all the tests passed, you now have an 'editable' install of isign. Any edits to this repo will affect (for instance)
how the `isign` command line tool works.
Sauce Labs supports ongoing public ``isign`` development. ``isign`` is a part of our infrastructure
for the `iOS Real Device Cloud <https://saucelabs.com/press-room/press-releases/sauce-labs-expands-mobile-test-automation-cloud-with-the-addition-of-real-devices-1>`__,
which allows customers to test apps and websites on real iOS devices. ``isign`` has been successfully re-signing submitted customer apps in production
since June 2015.
.. _More documentation:
More documentation
------------------
See the `docs <docs>`__ directory of this repository for random stuff that didn't fit here.
.. _Authors:
Authors
-------
`Neil Kandalgaonkar <https://github.com/neilk>`__ is the main developer and maintainer.
Proof of concept by `Steven Hazel <https://github.com/sah>`__ and Neil Kandalgaonkar.
Reference scripts using Apple tools by `Michael Han <https://github.com/mhan>`__.
================================================
FILE: apple/README.rst
================================================
These scripts will re-provision (aka resign, code sign, etc) an app,
using Apple tools. The isign tools emulate what these do, on Linux.
provisions.sh is the original script that Mike Han (@mikehan) wrote.
provisions.py is a python port.
================================================
FILE: apple/provisions.py
================================================
#!/usr/bin/env python
# Port of @mikehan's provisions.sh
import argparse
import glob
import os
import os.path
import shutil
from subprocess import call, Popen
import tempfile
CODESIGN_BIN = '/usr/bin/codesign'
PLIST_BUDDY_BIN = '/usr/libexec/PlistBuddy'
SECURITY_BIN = '/usr/bin/security'
ZIP_BIN = '/usr/bin/zip'
UNZIP_BIN = '/usr/bin/unzip'
class ReceivedApp(object):
def __init__(self, path):
self.path = path
def unpack_to_dir(self, unpack_dir):
app_name = os.path.basename(self.path)
target_dir = os.path.join(unpack_dir, app_name)
shutil.copytree(self.path, target_dir)
return App(target_dir)
class ReceivedIpaApp(ReceivedApp):
def unpack_to_dir(self, target_dir):
call([UNZIP_BIN, "-qu", self.path, "-d", target_dir])
return IpaApp(target_dir)
class App(object):
def __init__(self, path):
self.path = path
self.entitlements_path = os.path.join(self.path,
'Entitlements.plist')
self.app_dir = self.get_app_dir()
self.provision_path = os.path.join(self.app_dir,
'embedded.mobileprovision')
def get_app_dir(self):
return self.path
def provision(self, provision_path):
print "provision_path: {0}".format(provision_path)
shutil.copyfile(provision_path, self.provision_path)
def create_entitlements(self):
# we decode part of the provision path, then extract the
# Entitlements part, then write that to a file in the app.
# piping to Plistbuddy doesn't seem to work :(
# hence, temporary intermediate file
decoded_provision_fh, decoded_provision_path = tempfile.mkstemp()
decoded_provision_fh = open(decoded_provision_path, 'w')
decode_args = [SECURITY_BIN, 'cms', '-D', '-i', self.provision_path]
process = Popen(decode_args, stdout=decoded_provision_fh)
# if we don't wait for this to complete, it's likely
# the next part will see a zero-length file
process.wait()
get_entitlements_cmd = [
PLIST_BUDDY_BIN,
'-x',
'-c',
'print :Entitlements ',
decoded_provision_path]
entitlements_fh = open(self.entitlements_path, 'w')
process2 = Popen(get_entitlements_cmd, stdout=entitlements_fh)
process2.wait()
entitlements_fh.close()
# should destroy the file
decoded_provision_fh.close()
def codesign(self, certificate, path, extra_args=[]):
call([CODESIGN_BIN, '-f', '-s', certificate] + extra_args + [path])
def sign(self, certificate):
# first sign all the dylibs
frameworks_path = os.path.join(self.app_dir, 'Frameworks')
if os.path.exists(frameworks_path):
dylibs = glob.glob(os.path.join(frameworks_path, '*.dylib'))
for dylib in dylibs:
self.codesign(certificate, dylib)
# then sign the app
self.codesign(certificate,
self.app_dir,
['--entitlements', self.entitlements_path])
def package(self, output_path):
if not output_path.endswith('.app'):
output_path = output_path + '.app'
os.rename(self.app_dir, output_path)
return output_path
class IpaApp(App):
def _get_payload_dir(self):
return os.path.join(self.path, "Payload")
def get_app_dir(self):
glob_path = os.path.join(self._get_payload_dir(), '*.app')
apps = glob.glob(glob_path)
count = len(apps)
if count != 1:
err = "Expected 1 app in {0}, found {1}".format(glob_path, count)
raise Exception(err)
return apps[0]
def package(self, output_path):
if not output_path.endswith('.ipa'):
output_path = output_path + '.ipa'
temp = "out.ipa"
# need to chdir and use relative paths, because zip is stupid
old_cwd = os.getcwd()
os.chdir(self.path)
relative_payload_path = os.path.relpath(self._get_payload_dir(),
self.path)
call([ZIP_BIN, "-qr", temp, relative_payload_path])
os.rename(temp, output_path)
os.chdir(old_cwd)
return output_path
def absolute_path_argument(path):
return os.path.abspath(path)
def exists_absolute_path_argument(path):
if not os.path.exists(path):
raise argparse.ArgumentTypeError("%s does not exist!" % path)
return absolute_path_argument(path)
def app_argument(path):
path = exists_absolute_path_argument(path)
_, extension = os.path.splitext(path)
if extension == '.app':
app = ReceivedApp(path)
elif extension == '.ipa':
app = ReceivedIpaApp(path)
else:
raise argparse.ArgumentTypeError(
"{0} doesn't seem to be an .app or .ipa".format(path))
return app
def parse_args():
parser = argparse.ArgumentParser(
description='Resign an iOS application with a new identity '
'and provisioning profile.')
parser.add_argument(
'-p', '--provisioning-profile',
dest='provisioning_profile',
required=True,
metavar='<your.mobileprovision>',
type=exists_absolute_path_argument,
help='Path to provisioning profile')
parser.add_argument(
'-c', '--certificate',
dest='certificate',
required=True,
metavar='<certificate>',
help='Identifier for the certificate in your keychain. '
'See `security find-identity` for a list, or '
'`man codesign` for valid ways to specify it.')
parser.add_argument(
'-s', '--staging',
dest='stage_dir',
required=False,
metavar='<path>',
type=absolute_path_argument,
default=os.path.join(os.getcwd(), 'stage'),
help='Path to stage directory.')
parser.add_argument(
'-o', '--output',
dest='output_path',
required=False,
metavar='<path>',
type=absolute_path_argument,
default=os.path.join(os.getcwd(), 'out'),
help='Path to output file or directory')
parser.add_argument(
'app',
nargs=1,
metavar='<path>',
type=app_argument,
help='Path to application to re-sign, typically a '
'directory ending in .app or file ending in .ipa.')
return parser.parse_args()
if __name__ == '__main__':
args = parse_args()
received_app = args.app[0]
if os.path.exists(args.stage_dir):
shutil.rmtree(args.stage_dir)
os.mkdir(args.stage_dir)
app = received_app.unpack_to_dir(args.stage_dir)
app.provision(args.provisioning_profile)
app.create_entitlements()
app.sign(args.certificate)
output_path = app.package(args.output_path)
if os.path.exists(args.stage_dir):
shutil.rmtree(args.stage_dir)
print "Re-signed package: {0}".format(output_path)
================================================
FILE: apple/provisions.sh
================================================
#!/bin/bash
usage() {
echo "./provisions -p [PATH_TO_NEW_PROVISIONING_PROFILE] -c \"CERT NAME: MUST BE IN KEYCHAIN\" ipa_file"
exit
}
while getopts ":p:c:" opt; do
case $opt in
p ) PRO_PROFILE=$OPTARG ;;
c ) CERT_NAME=$OPTARG ;;
/? ) usage
esac
done
shift $(($OPTIND - 1))
if [[ -z "$@" ]]; then
usage
else
APP=$@
fi
verify_args() {
if [[ ! -e $APP ]]; then
echo "$APP does not exist"
exit 1
elif [[ ! -e $PRO_PROFILE ]]; then
echo "$PRO_PROFILE does not exist"
exit 1
elif [[ -z $CERT_NAME ]]; then
echo "Must specify a certificate to use"
exit 1
fi
}
is_app() {
[[ $APP =~ \.app$ ]]
}
is_ipa() {
[[ $APP =~ \.ipa$ ]]
}
setup_dir() {
STAGE_DIR=./stage
ENTITLEMENTS_FILE=$STAGE_DIR/Entitlements.plist
if [[ -e $STAGE_DIR ]]; then
rm -r $STAGE_DIR
fi
mkdir $STAGE_DIR
if is_app; then
cp -r $APP $STAGE_DIR
APP_NAME=$(basename $APP)
PAYLOAD_DIR=""
APP_DIR=$STAGE_DIR/$APP_NAME
elif is_ipa; then
unzip -qu $APP -d $STAGE_DIR
PAYLOAD_DIR=$STAGE_DIR/Payload
APP_DIR=$PAYLOAD_DIR/*.app
else
echo "Must provide either an .app or .ipa file"
exit 1
fi
}
copy_profile() {
cp "$PRO_PROFILE" $APP_DIR/embedded.mobileprovision
}
create_entitlements() {
/usr/libexec/PlistBuddy -x -c "print :Entitlements " /dev/stdin <<< $(security cms -D -i ${APP_DIR}/embedded.mobileprovision) > $ENTITLEMENTS_FILE
}
sign_app() {
if [ -e $APP_DIR/Frameworks ]; then
for dylib in "$APP_DIR/Frameworks/*"
do
echo "signing $dylib"
# entitlements are irrelevant to dylibs
/usr/bin/codesign -f -s "$CERT_NAME" $dylib
done
fi
echo "signing $APP_DIR";
/usr/bin/codesign -f -s "$CERT_NAME" --entitlements $ENTITLEMENTS_FILE $APP_DIR 2>/dev/null
}
package_app() {
if is_ipa; then
(cd $STAGE_DIR; zip -qr out.ipa Payload)
echo "Re-provisioned ipa at $STAGE_DIR/out.ipa"
else
echo "Re-provisioned app at $APP_DIR"
fi
}
verify_args
setup_dir
copy_profile
create_entitlements
sign_app
package_app
================================================
FILE: bin/isign
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import argparse
from isign import isign
from os.path import abspath, expanduser
import logging
FORMATTER = logging.Formatter('%(message)s')
def log_to_stderr(level=logging.INFO):
root = logging.getLogger()
root.setLevel(level)
handler = logging.StreamHandler()
handler.setFormatter(FORMATTER)
root.addHandler(handler)
def absolute_path_argument(path):
return abspath(expanduser(path))
def parse_args():
# note that for arguments which eventually get fed into
# isign.resign, we deliberately don't set defaults. The kwarg
# defaults in isign.resign will be used
parser = argparse.ArgumentParser(
description='Resign an iOS application with a new identity '
'and provisioning profile. See documentation for '
'how to obtain properly formatted credentials.')
parser.add_argument(
'-p', '--provisioning-profile',
dest='provisioning_profile',
required=False,
metavar='<your.mobileprovision path>',
type=absolute_path_argument,
help='Path to provisioning profile'
)
parser.add_argument(
'-a', '--apple-cert',
dest='apple_cert',
required=False,
metavar='<apple cert>',
type=absolute_path_argument,
help='Path to Apple certificate in PEM form. You only need to specify '
'this if the Apple certificate in the isign package is out of '
'date.'
)
parser.add_argument(
'-k', '--key',
dest='key',
required=False,
metavar='<key path>',
type=absolute_path_argument,
help='Path to your organization\'s key in PEM format.'
)
parser.add_argument(
'-c', '--certificate',
dest='certificate',
required=False,
metavar='<certificate path>',
type=absolute_path_argument,
help='Path to your organization\'s certificate in PEM format'
)
parser.add_argument(
'-n', '--credentials',
dest='credentials_dir',
required=False,
metavar='<directory>',
help='Equivalent to '
'-k <directory>/key.pem '
'-c <directory>/certificate.pem '
'-p <directory>/isign.mobileprovision'
)
parser.add_argument(
'-o', '--output',
dest='output_path',
required=False,
metavar='<output path>',
type=absolute_path_argument,
help='Path to output file or directory'
)
parser.add_argument(
'app_paths',
nargs=1,
metavar='<app path>',
type=absolute_path_argument,
help='Path to application to re-sign, typically a '
'directory ending in .app or file ending in .ipa.'
)
parser.add_argument(
'-v', '--verbose',
dest='verbose',
action='store_true',
default=False,
required=False,
help='Set logging level to debug.'
)
parser.add_argument(
'-i', '--info',
dest='info_props',
required=False,
metavar='<Info.plist properties>',
help='List of comma-delimited key=value pairs of Info.plist properties to override'
)
parser.add_argument(
'-d', '--display',
dest='display_only',
action='store_true',
default=False,
required=False,
help='Display information about the app without resigning'
)
parser.add_argument(
'-e', '--entitlements',
dest='alternate_entitlements_path',
required=False,
metavar='<alternate entitlements path>',
type=absolute_path_argument,
help='Sign with these entitlements, rather than ones extracted from provisioning profile'
)
return parser.parse_args()
def filter_args(args, interested):
""" Filter all args to args that we are interested in """
# We want the unused command line args to be
# missing in kwargs, so the defaults are used
kwargs = {}
for k, v in vars(args).iteritems():
if k in interested and v is not None:
kwargs[k] = v
return kwargs
if __name__ == '__main__':
args = parse_args()
if args.verbose:
level = logging.DEBUG
else:
level = logging.INFO
log_to_stderr(level)
if args.display_only:
# Only show information
import json
bundle_info = isign.view(args.app_paths[0])
print json.dumps(bundle_info, indent=4, separators=(',', ': '))
else:
# Handle the various kinds of resign operations
kwargs = {}
# There's only one output path, so it doesn't make sense
# to have multiple input paths
app_path = args.app_paths[0]
# Convert the Info.plist property pairs to a dict format
if args.info_props:
info_props = {}
for arg in args.info_props.split(','):
i = arg.find('=')
if i < 0:
raise Exception('Invalid Info.plist argument: ' + arg)
info_props[arg[0:i]] = arg[i + 1:]
if info_props:
kwargs['info_props'] = info_props
if args.credentials_dir:
# Handle a resign with credentials directory.
# First check they haven't over-specified credential paths
incompatible_args = ['certificate', 'key', 'provisioning_profile']
args_dict = vars(args)
for k, v in vars(args).iteritems():
if k in incompatible_args and v is not None:
raise Exception("Incompatible arguments. Do not use any of " +
", ".join(['--' + s for s in incompatible_args]) + " "
"with the --credentials argument.")
# looks good, now massage args into method arguments
resign_args = ['apple_cert',
'output_path']
kwargs.update(filter_args(args, resign_args))
isign.resign_with_creds_dir(app_path,
args.credentials_dir,
**kwargs)
else:
# Handle standard resign. User may have specified all, some
# or none of the credential files, in which case we rely on
# isign.resign() to supply defaults.
# Massage args into method arguments
resign_args = ['certificate',
'key',
'apple_cert',
'provisioning_profile',
'output_path',
'alternate_entitlements_path']
kwargs.update(filter_args(args, resign_args))
isign.resign(app_path, **kwargs)
================================================
FILE: bin/isign_export_creds.sh
================================================
#!/usr/bin/env bash
# Export a certificate and key from a .p12 file into PEM form, and place
# them where isign expects them to be.
#
# The .p12 file is typically exported from Apple's Keychain Access.
if [[ $# -eq 0 ]]; then
echo "Usage: $0 exported.p12 [target_directory]"
exit 1;
fi
p12_path=$1
if [[ ! -e $p12_path ]]; then
echo "Can't find $p12_path!";
exit 1;
fi
target_dir=${2-$HOME/.isign}
target_cert_path=$target_dir/certificate.pem
target_key_path=$target_dir/key.pem
chmod 600 $p12_path
mkdir -p $target_dir
openssl pkcs12 -in $p12_path -out $target_cert_path -clcerts -nokeys
openssl pkcs12 -in $p12_path -out $target_key_path -nocerts -nodes
chmod 600 $target_key_path
read -p "Done exporting $p12_path. Remove it? [Y/n]:" -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
rm $p12_path
fi
echo "Exported credentials from $p12_path to $target_dir"
read -p "Find matching provisioning profile? [Y/n]:" -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
./guess_mobileprovision.sh $target_cert_path
fi
================================================
FILE: bin/isign_guess_mobileprovision.sh
================================================
#!/usr/bin/env bash
# given the filename certificate in PEM form, find potentially matching .mobileprovision files
# in the usual directory on Mac OS X
PROVISIONING_PROFILE_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"
cert_path=${1-$HOME/.isign/certificate.pem}
echo "Looking for provisioning profiles signed with $cert_path..."
get_cert() {
cert_path=$1
in_certificate=1
certificate=''
while read line; do
if [[ $line =~ 'BEGIN CERTIFICATE' ]]; then
in_certificate=0
continue
fi
if [[ $line =~ 'END CERTIFICATE' ]]; then
in_certificate=1
continue
fi
if [[ $in_certificate -eq 0 ]]; then
# trim leading/trailing whitespace
line="$(echo -e $line | sed -e 's/^[[:space:]]*//' | sed -e 's/[[:space:]]*$//')"
certificate="${certificate}${line}"
fi
done < $cert_path
echo $certificate
}
target_cert=$(get_cert $cert_path)
find "$PROVISIONING_PROFILE_DIR/mobdev1.mobileprovision" -type f -print0 | while IFS= read -r -d '' mobileprovision; do
# extract the cert from the mobileprovision with `security`
mobileprovision_data=$(security cms -D -i "$mobileprovision")
# PlistBuddy doesn't give array lengths, so we don't know how many certs this mobileprovision might have. Try a few
for i in `seq 0 10`; do
# because Plistbuddy is dumb, we have to use a convoluted shell syntax to read from stdin
# finally base64 it so we can do an easy string comparison
# TODO: can't quite figure out why, but I need to pipe it to base64, can't do that encoding later.
# But this means I can't figure out if the PlistBuddy succeeded or failed, by checking $?
# Have to swallow the error, iterate more times than necessary :(
cert=$(/usr/libexec/PlistBuddy -c "Print :DeveloperCertificates:$i" /dev/stdin <<< $(echo $mobileprovision_data) 2>/dev/null | base64)
# Examine a long prefix (there are some issues with padding & zeroes at the end).
# if first thousand match it is almost certainly a match
if [[ ${cert:0:1000} = ${target_cert:0:1000} ]]; then
echo "\"$mobileprovision\" was signed with this certificate."
fi
done
done
================================================
FILE: bin/make_seal
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from optparse import OptionParser
import isign.code_resources
import logging
FORMATTER = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
def log_to_stderr(level=logging.INFO):
root = logging.getLogger()
root.setLevel(level)
handler = logging.StreamHandler()
handler.setFormatter(FORMATTER)
root.addHandler(handler)
if __name__ == '__main__':
log_to_stderr()
parser = OptionParser()
options, args = parser.parse_args()
source_app, target_dir = args
isign.code_resources.make_seal(source_app, target_dir)
================================================
FILE: bin/multisign
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# From a single IPA, generate multiple re-signed IPAs simultaneously.
# Why? Because you might want to distribute an app to a lot of organizations at once,
# or perhaps you need to sign for an enterprise and a local debug deployment all at
# the same time, and you want it to be fast.
# Depends on the existence of external `zip` and `unzip` programs.
import argparse
from os.path import abspath, basename, dirname, expanduser, join
from isign.multisign import multisign
import logging
FORMATTER = logging.Formatter('%(message)s')
log = logging.getLogger(__name__)
def log_to_stderr(level=logging.INFO):
root = logging.getLogger()
root.setLevel(level)
handler = logging.StreamHandler()
handler.setFormatter(FORMATTER)
root.addHandler(handler)
def absolute_path_argument(path):
return abspath(expanduser(path))
def parse_args():
parser = argparse.ArgumentParser(
description='From a single IPA, generate multiple re-signed IPAs simultaneously')
parser.add_argument(
'-a', '--app',
dest='app',
required=True,
metavar='<file>',
type=absolute_path_argument,
help='Path to input app'
)
parser.add_argument(
'credential_dirs',
nargs='+',
metavar='<directory>',
type=absolute_path_argument,
help='Paths to directories containing credentials with standardized names'
)
parser.add_argument(
'-i', '--info',
dest='info_props',
required=False,
metavar='<Info.plist properties>',
help='List of comma-delimited key=value pairs of Info.plist properties to override'
)
parser.add_argument(
'-v', '--verbose',
dest='verbose',
action='store_true',
default=False,
required=False,
help='Set logging level to debug.'
)
return parser.parse_args()
def get_output_path(original_path, credential_dir):
""" Create some filename based on the credential directory and original path """
original_name = basename(original_path)
original_dir = dirname(original_path)
resigned_name = basename(credential_dir) + '_' + original_name
return join(original_dir, resigned_name)
if __name__ == '__main__':
args = parse_args()
if args.verbose:
level = logging.DEBUG
else:
level = logging.INFO
log_to_stderr(level)
# Convert the Info.plist property pairs to a dict format
info_props = None
if args.info_props:
info_props = {}
for arg in args.info_props.split(','):
i = arg.find('=')
if i < 0:
raise Exception('Invalid Info.plist argument: ' + arg)
info_props[arg[0:i]] = arg[i + 1:]
log.debug('got credential paths: {}'.format(', '.join(args.credential_dirs)))
log.debug('got incoming app: {}'.format(args.app))
# ensure basenames are unique
unique_basenames = set([basename(p) for p in args.credential_dirs])
if len(args.credential_dirs) != len(unique_basenames):
raise Exception('Credential directories do not have unique basenames. '
'Cannot name output archives automatically. Sorry!')
credential_dirs_to_output_paths = {}
for d in args.credential_dirs:
credential_dirs_to_output_paths[d] = get_output_path(args.app, d)
results = multisign(args.app, credential_dirs_to_output_paths, info_props)
for credentials_dir, resigned_app_path in results:
log.info("resigned with %s to %s", credentials_dir, resigned_app_path)
================================================
FILE: bin/pprint_codesig
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import argparse
import isign.archive
from isign.exceptions import NotSignable
from isign.signable import Executable
import logging
from os.path import abspath, expanduser, isdir
import shutil
FORMATTER = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
log = logging.getLogger(__name__)
def log_to_stderr(level=logging.INFO):
root = logging.getLogger()
root.setLevel(level)
handler = logging.StreamHandler()
handler.setFormatter(FORMATTER)
root.addHandler(handler)
def absolute_path_argument(path):
return abspath(expanduser(path))
def print_codesig(path):
temp_dir = None
try:
archive = isign.archive.archive_factory(path)
(temp_dir, bundle) = appArchive.unarchive_to_temp()
_print_codesig(bundle)
except NotSignable as e:
msg = "Not signable: <{0}> {1}\n".format(path, e)
log.error(msg)
raise
finally:
if temp_dir is not None and isdir(temp_dir):
shutil.rmtree(temp_dir)
def _print_codesig(bundle):
executable = Executable(bundle.get_executable_path())
for arch in executable.arches:
print arch['cmds']['LC_CODE_SIGNATURE']
def parse_args():
parser = argparse.ArgumentParser(
description='Print the code signature structure of an executable or dylib')
parser.add_argument(
'-v', '--verbose',
dest='verbose',
action='store_true',
default=False,
required=False,
help='Set logging level to debug.'
)
parser.add_argument(
'app_paths',
nargs=1,
metavar='<app path>',
type=absolute_path_argument,
help='Path(s) to application, typically a '
'directory ending in .app or file ending in .ipa.'
)
return parser.parse_args()
if __name__ == '__main__':
args = parse_args()
if args.verbose:
level = logging.DEBUG
else:
level = logging.INFO
log_to_stderr(level)
print_codesig(args.app_paths[0])
================================================
FILE: dev/requirements.txt
================================================
nose
pytest
twine
================================================
FILE: dev/setup.sh
================================================
#!/bin/bash
set -e
# determine local paths
pushd $(dirname "$0") >/dev/null
DEV_DIR=$PWD
cd ..
SRC_DIR=$PWD
popd >/dev/null
pip install -U -r ${DEV_DIR}/requirements.txt
../INSTALL.sh develop
================================================
FILE: docs/applecerts.rst
================================================
Apple certificates
==================
You probably don't need to change this file, not for a long time.
The ``applecerts.pem`` file can be constructed by these steps. In theory
you can export them from Keychain Access, too, but here's a procedure that
doesn't involve an Apple machine. This worked for us in June 2016:
.. code:: bash
$ curl 'https://www.apple.com/appleca/AppleIncRootCertificate.cer' > AppleIncRootCertificate.cer
$ curl 'https://developer.apple.com/certificationauthority/AppleWWDRCA.cer' > AppleWWDRCA.cer
$ openssl x509 -inform der -in AppleIncRootCertificate.cer -outform pem -out AppleIncRootCertificate.pem
$ openssl x509 -inform der -in AppleWWDRCA.cer -outform pem -out AppleWWDRCA.pem
$ cat AppleWWDRCA.pem AppleIncRootCertificate.pem > applecerts.pem
Here's a conceptual explanation of what we're doing:
Download the following certs from `Apple's Certificate Authority
Page <https://www.apple.com/certificateauthority/>`__
- Apple Inc. Root Certificate
- Worldwide Developer Relations Certificate
Then convert these to
`PEM <http://en.wikipedia.org/wiki/Privacy-enhanced_Electronic_Mail>`__
format.
Then, concatenate them together. This file can now serve as the 'Apple
certificate' for code signing.
================================================
FILE: docs/codedirectory.rst
================================================
CodeDirectory slots
===================
A signature is mostly composed of hashes of blocks of the file's
contents. However, at some point, Apple added special hashes so the
state of other resources in the app could be captured in the signature.
For instance, the Info.plist gets its own hash, and ultimately the
hashes of all the other files are also captured in the ResourceDirectory
hash. Together, all these special hashes are called the CodeDirectory.
Perhaps to indicate that these are special hashes, they were given
negative offsets in the list of hashes.
For instance, if you do ``codesign -d -r- --verbose=20 some.app``
::
Executable=...
Identifier=com.somecompany.someapp
Format=bundle with Mach-O universal (armv7 arm64)
CodeDirectory v=20200 size=874 flags=0x0(none) hashes=35+5 location=embedded
Hash type=sha1 size=20
-5=0ea763a5bc4d19b0e03315a956deecd97693a661
-4=0000000000000000000000000000000000000000
-3=b353e6ce8464fd8ae32cfcf09e7c9015b7378054
-2=32a5edb9b03a0bea2d7bc30cfdddadab7dab841c
-1=46ebe92997b23b2e2187a21714c8cc32c347bf35
0=70e024fdab3426c375cf283d384de58ec6fff438
1=1ceaf73df40e531df3bfb26b4fb7cd95fb7bff1d
2=1ceaf73df40e531df3bfb26b4fb7cd95fb7bff1d
...
The CodeDirectory hashes have stable negative indices - for instance, -1
is always the hash of the Info.plist file. The indices for the
CodeDirectory hashes are sometimes called slots.
When building the CodeDirectory, We need to observe these constraints:
- Executables should have all 5 slots in their codedirectory
- Dylibs only need 2 slots, but sometimes have been compiled with 5
- Dylibs should never include the ResourceDir slot, even if they have 5
slots
- We should delay calculating hashes until we know we are going to use
them
- Nobody uses the Application-specific slot anyway
- At least so far, we don't need to change the Info.plist slot when
re-signing
================================================
FILE: docs/credentials.rst
================================================
Apple developer credentials
===========================
Mac OS X and iOS aren't like most other operating systems -- security is baked into every
single program. The machine is able to check, before it even executes the program, who
wrote it, whether Apple approved it, and whether it has the authority to do some things
that it's asking to do. On Mac OS X, it's easy to get around those restrictions, but iOS
is locked down very tight.
The iOS device is able tell who wrote a program, and whether Apple approved it, without
even phoning home to Apple. It does this by looking for evidence that is *was* approved
at some earlier date, using cryptographic signatures embedded right into the application.
Until now, few people outside Apple had a full understanding of how that worked, or how to
add that magic "code signature" and other necessary proofs to the app.
``isign`` is able to create those -- with the right credentials.
Definitions
-----------
The **key** is the developer's private key. If you're familiar with Secure Shell keys, this is similar -- it's
the private half of a public-private key pair. The private key is used to sign the application. This proves that
the developer approved the entire contents of the application.
Note that this key is not revealed to Apple, or anyone else. If you
develop iOS apps, you, or someone in your organization, generated such a key. Hopefully they kept it
secret. However, you need it to sign the app -- you probably have it in Keychain Access or something like
that.
The **certificate** is how Apple indicates that the developer is approved to write apps for iOS. At some
point, they signed the public half of the developer's keys. Apple will also encode when your developer account
is going to expire.
The **organizational unit** is an identifier that Apple assigns to organizations that
create apps on Apple devices. It's an eight character code of letters and numbers. You may need to know your
organizational unit, so you can sign apps from your organization. ``isign`` can extract this from other files,
like your certificate, so you probably don't need to worry about this.
The **provisioning profile** is where it all comes together. This file is included in the app.
The provisioning profile includes the contents of the certificate -- possibly many certificates.
It also includes the UDIDs of the devices that you have registered with Apple. So, you can often use the same
provisioning profile for all your developers and all your iOS devices.
So, when a phone encounters an app, it will start by reading the provisioning profile, to learn things like:
- Who wrote this app?
- What organization do they belong to?
- Are they still an Apple-approved developer?
- And what's their public key?
- Is this app approved to run on this particular device?
From there it can go on to cryptographically verify each and every part of the app. This ensures that the
app has not been tampered with, or that an unapproved developer hasn't snuck an app onto your phone (even if
that developer is you.)
How to create Apple developer credentials
-----------------------------------------
For most developers, XCode takes care of most of these credentials. But with ``isign`` you have to do it more manually.
Getting an account
~~~~~~~~~~~~~~~~~~
First, find an administrator for your developer organization at Apple. (If you're a solo developer, that's you.)
Get them to invite you to your Apple Organizational Unit.
You'll get email from Apple, which will prompt you to set up your account. Set up passwords and so on as usual.
Troubleshooting: you may have to click on the mailed invite link
once to set up your account, and then return to your mail to click
on that invite link again to actually activate your account. Also,
in general, things on the Apple site work better with Safari, so
if something doesn't work, try that browser.
Setting up credentials
~~~~~~~~~~~~~~~~~~~~~~
The following procedure works as of June 2015, for adding a new
account from scratch. You will need a Mac OS X computer. (In theory
you could do this entirely from Linux, using the Apple website, but
you're on your own there.)
Log into your Apple account.
Go to **Certificates, Identifiers, & Profiles**.
Go to **Certificates > Development** in the left-hand column.
Press the plus icon to add a new certificate.
It will ask what type of certificate you need. You probably want
*iOS App Development*.
It will then instruct you to use Keychain Access to generate a
Certificate Signing Request. In effect you are going to create a
private/public key pair, and then make a little file that says "Hey
Apple, please sign the public half and make a certificate for it, and
associate that with my Apple account!"
Follow the instructions and save that CSR to disk. Press **Continue**.
Then, the Apple website will ask you to upload that CSR. Do so, and
it will create a certificate for your account in your organization.
This certificate might need to be approved by an admin before you
can download it.
Once it's approved, download it!
It will probably be named something generic like ``ios_development.cer``,
so rename it to something more meaningful and put it somewhere safe.
Import that ``.cer`` into Keychain. Keychain will detect that it
has an associated private key, and in views where you see keys, the
certificate will be "inside" the key, and vice versa.
Provisioning profile
~~~~~~~~~~~~~~~~~~~~
Now we just need to tell Apple that your user is allowed to deploy on those devices.
First, go to **Devices**, and add the UDIDs of all the devices you care about. If you installed `libimobiledevice`, an easy way to get the
UDID is with ``idevice_id -l``.
In the Developer portal, go to **Provisioning Profiles**, and create a new development profile. Select those devices and add them.
In **Select App ID**, you probably want to just create one with a
wildcard (Something using your Apple Organizational Unit plus domain plus dot-star, maybe,
like ``A1B2C3D4.tld.domain.*`` )
Next, in **'Select certificates'**, select the certificates you want, which probably includes the one we just created above.
Finally, download this provisioning profile, and follow the instructions in the main README.
================================================
FILE: docs/speed.rst
================================================
So you want to make it go faster
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The *vast* majority of time in isign is not spent signing. It's in zipping and re-zipping.
Most apps are packaged as IPAs. These are just glorified zip files with a particular directory structure. For most situations, we can't avoid the cost of unzipping and re-zipping. So if we're trying to make it faster, we've already lost, since we'll never speed it up even by a factor of 2.
In theory you could:
- Not use IPAs. isign can also re-sign unzipped ".app" directories. This is way faster, but then you have to use isign where you created the app, and you have to install the resigned app onto a device from there as well.
- Use IPAs but re-compress them poorly. Basically, change the setting from zip quality of 6 (the default) to something else. Minor speedups can be accomplished that way, at the cost of increasing install time or time to transfer to send it the network. In my tests it wasn't an improvement when you considered typical things you would do with the app next.
If neither of those approaches is acceptable to you, you can stop reading here. Because everything else that follows will speed it up, but not by much.
Okay, so what *else* is taking time?
1) Copying the files. Maybe with some exotic copy-on-write filesystem we could avoid that?
2) Hashing the entire contents of the application. For data that we've never seen before, we can't do better than O(n). So we lose *again*.
But we could still do better in the general case:
- There are many files within the application. If we don't touch a file (it's not an executable
or dylib), and we got a hash in the app's existing CodeResources seal, we could trust it, and
reuse it in our CodeResources seal.
I tried implementing this and it was super annoying to code, and didn't
have a big impact on my test files, so I gave up. The problem is that you have to write wrappers around
every method that writes to a file, and keep a tally of what files you touched. If you don't mind a less
accurate method (scan directories for files changed since the start of the resigning process) that
might work better.
- We could recognize common libraries such as the Swift framework, and keep re-signed versions of
those in some persistent storage.
- Use separate processes to hash files, to exploit multiple cores.
But wait!
~~~~~~~~~
Incidentally, if what you're looking for is to resign one app with multiple credentials, look into the `multisign` scripts. There can save a significant amount of time by unzipping the original app only once, and using a process pool to exploit multiple cores.
================================================
FILE: isign/__init__.py
================================================
# -*- coding: utf-8 -*-
import os.path
import json
package_dir = os.path.dirname(os.path.realpath(__file__))
with open(os.path.join(package_dir, "version.json"), 'r') as f:
version = json.load(f)
__version__ = version['version']
__commit__ = version['commit']
__build__ = version['build']
================================================
FILE: isign/apple_credentials/README.rst
================================================
These Apple credentials are public knowledge (see the docs) so they
are okay to distribute. Don't put anything else here.
================================================
FILE: isign/archive.py
================================================
# -*- coding: utf-8 -*-
""" Represents an app archive. This is an app at rest, whether it's a naked
app bundle in a directory, or a zipped app bundle, or an IPA. We have a
common interface to extract these apps to a temp file, then resign them,
and create an archive of the same type """
import abc
import biplist
from bundle import App, Bundle, is_info_plist_native
from exceptions import MissingHelpers, NotSignable, NotMatched
from distutils import spawn
import logging
import os
from os.path import abspath, dirname, exists, isdir, isfile, join, normpath
import tempfile
import re
from subprocess import call
from signer import Signer
import shutil
import zipfile
import utils
REMOVE_WATCHKIT = True
helper_paths = {}
log = logging.getLogger(__name__)
def get_helper(helper_name):
""" find paths to executables. Cached in helper_paths """
if helper_name not in helper_paths or helper_paths[helper_name] is None:
# note, find_executable returns None is not found
# in other words, we keep retrying until found
helper_paths[helper_name] = spawn.find_executable(helper_name)
log.debug("got executable {} for {}".format(helper_paths[helper_name],
helper_name))
return helper_paths[helper_name]
def make_temp_dir():
return tempfile.mkdtemp(prefix="isign-")
def get_watchkit_paths(root_bundle_path):
""" collect sub-bundles of this bundle that have watchkit """
# typical structure:
#
# app_bundle
# ...
# some_directory
# watchkit_extension <-- this is the watchkit bundle
# Info.plist
# watchkit_bundle <-- this is the part that runs on the Watch
# Info.plist <-- WKWatchKitApp=True
#
watchkit_paths = []
for path, _, _ in os.walk(root_bundle_path):
if path == root_bundle_path:
continue
try:
bundle = Bundle(path)
except NotMatched:
# this directory is not a bundle
continue
if bundle.info.get('WKWatchKitApp') is True:
# get the *containing* bundle
watchkit_paths.append(dirname(path))
return watchkit_paths
def process_watchkit(root_bundle_path, should_remove=False):
""" Unfortunately, we currently can't sign WatchKit. If you don't
care about watchkit functionality, it is
generally harmless to remove it, so that's the default.
Remove when https://github.com/saucelabs/isign/issues/20 is fixed """
watchkit_paths = get_watchkit_paths(root_bundle_path)
if len(watchkit_paths) > 0:
if should_remove:
for path in watchkit_paths:
log.warning("Removing WatchKit bundle {}".format(path))
shutil.rmtree(path)
else:
raise NotSignable("Cannot yet sign WatchKit bundles")
class Archive(object):
__metaclass__ = abc.ABCMeta
# we use abc.abstractmethod throughout because there are certain class
# methods we want to ensure are implemented.
@abc.abstractmethod
def unarchive_to_temp(self):
""" Unarchive and copy to a temp directory """
pass
@abc.abstractmethod
def archive(cls, path, output_path):
""" Archive a directory to an output path """
pass
@abc.abstractmethod
def get_info(cls, path):
""" Obtain app metadata from Info.plist without unarchiving """
pass
@abc.abstractmethod
def precheck(cls, path):
""" Check if this is, in fact, an archive of this type """
pass
@abc.abstractmethod
def find_bundle_dir(cls, path):
""" Locate the directory of the main app (aka bundle) """
pass
class AppArchive(Archive):
""" The simplest form of archive -- a naked App Bundle, with no extra directory structure,
compression, etc """
@classmethod
def find_bundle_dir(cls, path):
""" Included for similarity with the zipped archive classes. In this case, the bundle dir
*is* the directory """
return path
@classmethod
def _get_plist_path(cls, path):
return join(cls.find_bundle_dir(path), "Info.plist")
@classmethod
def get_info(cls, path):
return biplist.readPlist(cls._get_plist_path(path))
@classmethod
def precheck(cls, path):
if not isdir(path):
return False
if not os.path.exists(cls._get_plist_path(path)):
return False
plist = cls.get_info(path)
is_native = is_info_plist_native(plist)
log.debug("is_native: {}".format(is_native))
return is_native
@classmethod
def archive(cls, path, output_path):
if exists(output_path):
shutil.rmtree(output_path)
shutil.move(path, output_path)
log.info("archived %s to %s" % (cls.__name__, output_path))
def __init__(self, path):
self.path = path
self.relative_bundle_dir = '.'
self.bundle_info = self.get_info(self.path)
def unarchive_to_temp(self):
containing_dir = make_temp_dir()
log.debug("unarchiving to temp... %s -> %s", self.path, containing_dir)
shutil.rmtree(containing_dir) # quirk of copytree, top dir can't exist already
shutil.copytree(self.path, containing_dir)
process_watchkit(containing_dir, REMOVE_WATCHKIT)
return UncompressedArchive(containing_dir, '.', self.__class__)
class AppZipArchive(Archive):
""" Just like an app, except it's zipped up, and when repackaged,
should be re-zipped. """
app_dir_pattern = r'^([^/]+\.app/).*$'
extensions = ['.zip']
helpers = ['zip', 'unzip']
@classmethod
def is_helpers_present(cls):
""" returns False if any of our helper apps wasn't found in class init """
is_present = True
for helper_name in cls.helpers:
if get_helper(helper_name) is None:
log.error("missing helper for class {}: {}".format(cls.__name__, helper_name))
is_present = False
break
return is_present
@classmethod
def is_archive_extension_match(cls, path):
""" does this path have the right extension """
log.debug('extension match')
for extension in cls.extensions:
log.debug('extension match: %s', extension)
if path.endswith(extension):
return True
return False
@classmethod
def find_bundle_dir(cls, zipfile_obj):
relative_bundle_dir = None
apps = set()
file_list = zipfile_obj.namelist()
for file_name in file_list:
matched = re.match(cls.app_dir_pattern, file_name)
if matched:
apps.add(matched.group(1))
if len(apps) == 1:
log.debug("found one app")
relative_bundle_dir = apps.pop()
elif len(apps) > 1:
log.warning('more than one app found in archive')
else:
log.warning('no apps found in archive')
return relative_bundle_dir
@classmethod
def _get_plist_path(cls, relative_bundle_dir):
return join(relative_bundle_dir, "Info.plist")
@classmethod
def precheck(cls, path):
""" Checks if an archive looks like this kind of app. Have to examine
within the zipfile, b/c we don't want to make temp dirs just yet. This
recapitulates a very similar precheck in the Bundle class """
if not isfile(path):
return False
if not cls.is_helpers_present():
raise MissingHelpers("helpers not present")
is_native = False
log.debug('precheck')
log.debug('path: %s', path)
if (cls.is_archive_extension_match(path) and
zipfile.is_zipfile(path)):
log.debug("this is an archive, and a zipfile")
zipfile_obj = zipfile.ZipFile(path)
relative_bundle_dir = cls.find_bundle_dir(zipfile_obj)
if relative_bundle_dir is not None:
plist_path = cls._get_plist_path(relative_bundle_dir)
if plist_path not in zipfile_obj.namelist():
return False
plist = cls.get_info(relative_bundle_dir, zipfile_obj)
is_native = is_info_plist_native(plist)
log.debug("is_native: {}".format(is_native))
return is_native
@classmethod
def get_info(cls, relative_bundle_dir, zipfile_obj):
plist_path = cls._get_plist_path(relative_bundle_dir)
plist_bytes = zipfile_obj.read(plist_path)
return biplist.readPlistFromString(plist_bytes)
def __init__(self, path):
self.path = path
zipfile_obj = zipfile.ZipFile(path)
self.relative_bundle_dir = self.find_bundle_dir(zipfile_obj)
self.bundle_info = self.get_info(self.relative_bundle_dir,
zipfile_obj)
def unarchive_to_temp(self):
containing_dir = make_temp_dir()
call([get_helper('unzip'), "-qu", self.path, "-d", containing_dir])
app_dir = abspath(join(containing_dir, self.relative_bundle_dir))
process_watchkit(app_dir, REMOVE_WATCHKIT)
return UncompressedArchive(containing_dir, self.relative_bundle_dir, self.__class__)
@classmethod
def archive(cls, containing_dir, output_path):
""" archive this up into a zipfile. Note this is a classmethod, because
the caller will use us on a temp directory somewhere """
# the temp file is necessary because zip always adds ".zip" if it
# does not have an extension. But we want to respect the desired
# output_path's extension, which could be ".ipa" or who knows.
# So we move it to the output_path later.
#
# We also do a little dance with making another temp directory just
# to construct the zip file. This is the best way to ensure the an unused
# filename. Also, `zip` won't overwrite existing files, so this is safer.
temp_zip_dir = None
try:
# need to chdir and use relative paths, because zip is stupid
temp_zip_dir = tempfile.mkdtemp(prefix="isign-zip-")
temp_zip_file = join(temp_zip_dir, 'temp.zip')
call([get_helper('zip'), "-qr", temp_zip_file, "."], cwd=containing_dir)
shutil.move(temp_zip_file, output_path)
log.info("archived %s to %s" % (cls.__name__, output_path))
finally:
if temp_zip_dir is not None and isdir(temp_zip_dir):
shutil.rmtree(temp_zip_dir)
class IpaArchive(AppZipArchive):
""" IPA is Apple's standard for distributing apps. Much like an AppZip,
but slightly different paths """
extensions = ['.ipa']
app_dir_pattern = r'^(Payload/[^/]+\.app/).*$'
class UncompressedArchive(object):
""" This just keeps track of some state with an unzipped app archive and
how to re-zip it back up once re-signed. The bundle is located somewhere
inside the containing directory, but might be a few directories down, like in
a ContainingDir/Payload/something.app
This class is also useful if you have an app that's already unzipped and
you want to sign it. """
def __init__(self, path, relative_bundle_dir, archive_class):
""" Path is the "Containing dir", the dir at the root level of the unzipped archive
(or the dir itself, in the case of an AppArchive archive)
relative bundle dir is the dir containing the bundle, e.g. Payload/Foo.app
archive class is the kind of archive this was (Ipa, etc.) """
path = utils.remove_control_char(path)
self.path = path
self.relative_bundle_dir = relative_bundle_dir
self.archive_class = archive_class
bundle_path = normpath(join(path, relative_bundle_dir))
self.bundle = App(bundle_path)
def archive(self, output_path):
""" Re-zip this back up, or simply copy it out, depending on what the
original archive class did """
self.archive_class.archive(self.path, output_path)
def clone(self, target_path):
""" Copy the uncompressed archive somewhere else, return initialized
UncompressedArchive """
shutil.copytree(self.path, target_path)
return self.__class__(target_path,
self.relative_bundle_dir,
self.archive_class)
def remove(self):
# the containing dir might be gone already b/c AppArchive simply moves
# it to the desired target when done
if exists(self.path) and isdir(self.path):
log.debug('removing ua: %s', self.path)
shutil.rmtree(self.path)
def archive_factory(path):
""" Guess what kind of archive we are dealing with, return an
archive object. Returns None if path did not match any archive type """
archive = None
for cls in [IpaArchive, AppZipArchive, AppArchive]:
if cls.precheck(path):
archive = cls(path)
log.debug("File %s matched as %s", path, cls.__name__)
break
return archive
def view(input_path):
if not exists(input_path):
raise IOError("{0} not found".format(input_path))
ua = None
bundle_info = None
try:
archive = archive_factory(input_path)
if archive is None:
raise NotMatched('No matching archive type found')
ua = archive.unarchive_to_temp()
bundle_info = ua.bundle.info
finally:
if ua is not None:
ua.remove()
return bundle_info
def resign(input_path,
certificate,
key,
apple_cert,
provisioning_profile,
output_path,
info_props=None,
alternate_entitlements_path=None):
""" Unified interface to extract any kind of archive from
a temporary file, resign it with these credentials,
and create a similar archive for that resigned app """
if not exists(input_path):
raise IOError("{0} not found".format(input_path))
log.debug('Signing with apple_cert: {}'.format(apple_cert))
log.debug('Signing with key: {}'.format(key))
log.debug('Signing with certificate: {}'.format(certificate))
log.debug('Signing with provisioning_profile: {}'.format(provisioning_profile))
signer = Signer(signer_cert_file=certificate,
signer_key_file=key,
apple_cert_file=apple_cert)
ua = None
bundle_info = None
try:
archive = archive_factory(input_path)
if archive is None:
raise NotSignable('No matching archive type found')
ua = archive.unarchive_to_temp()
if info_props:
# Override info.plist props of the parent bundle
ua.bundle.update_info_props(info_props)
ua.bundle.resign(signer, provisioning_profile, alternate_entitlements_path)
bundle_info = ua.bundle.info
ua.archive(output_path)
except NotSignable as e:
msg = "Not signable: <{0}>: {1}\n".format(input_path, e)
log.info(msg)
raise
finally:
if ua is not None:
ua.remove()
return bundle_info
================================================
FILE: isign/bundle.py
================================================
# -*- coding: utf-8 -*-
""" Represents a bundle. In the words of the Apple docs, it's a convenient way to deliver
software. Really it's a particular kind of directory structure, with one main executable,
well-known places for various data files and libraries,
and tracking hashes of all those files for signing purposes.
For isign, we have two main kinds of bundles: the App, and the Framework (a reusable
library packaged along with its data files.) An App may contain many Frameworks, but
a Framework has to be re-signed independently.
See the Apple Developer Documentation "About Bundles" """
import biplist
import code_resources
from exceptions import NotMatched
import copy
import glob
import logging
import os
from os.path import basename, exists, join, splitext
from signer import openssl_command
import signable
import shutil
import utils
log = logging.getLogger(__name__)
def is_info_plist_native(plist):
""" If an bundle is for native iOS, it has these properties in the Info.plist """
return (
'CFBundleSupportedPlatforms' in plist and
'iPhoneOS' in plist['CFBundleSupportedPlatforms']
)
class Bundle(object):
""" A bundle is a standard directory structure, a signable, installable set of files.
Apps are Bundles, but so are some kinds of Frameworks (libraries) """
helpers = []
signable_class = None
entitlements_path = None # Not set for every bundle type
def __init__(self, path):
# Remove Control character
path = utils.remove_control_char(path)
self.path = path
self.info_path = join(self.path, 'Info.plist')
if not exists(self.info_path):
raise NotMatched("no Info.plist found; probably not a bundle")
self.info = biplist.readPlist(self.info_path)
self.orig_info = None
if not is_info_plist_native(self.info):
# while we should probably not allow this *or* add it ourselves, it appears to work without it
log.debug(u"Missing/invalid CFBundleSupportedPlatforms value in {}".format(self.info_path))
# will be added later
self.seal_path = None
def get_entitlements_path(self):
return self.entitlements_path
def get_executable_path(self):
""" Path to the main executable. For an app, this is app itself. For
a Framework, this is the main framework """
executable_name = None
if 'CFBundleExecutable' in self.info:
executable_name = self.info['CFBundleExecutable']
else:
executable_name, _ = splitext(basename(self.path))
executable_name = utils.remove_control_char(executable_name)
executable = join(self.path, executable_name)
if not exists(executable):
raise Exception(
'could not find executable for {0}'.format(self.path))
return executable
def update_info_props(self, new_props):
if self.orig_info is None:
self.orig_info = copy.deepcopy(self.info)
changed = False
if ('CFBundleIdentifier' in new_props and
'CFBundleURLTypes' in self.info and
'CFBundleURLTypes' not in new_props):
# The bundle identifier changed. Check CFBundleURLTypes for
# CFBundleURLName values matching the old bundle
# id if it's not being set explicitly
old_bundle_id = self.info['CFBundleIdentifier']
new_bundle_id = new_props['CFBundleIdentifier']
for url_type in self.info['CFBundleURLTypes']:
if 'CFBundleURLName' not in url_type:
continue
if url_type['CFBundleURLName'] == old_bundle_id:
url_type['CFBundleURLName'] = new_bundle_id
changed = True
for key, val in new_props.iteritems():
is_new_key = key not in self.info
if is_new_key or self.info[key] != val:
if is_new_key:
log.warn("Adding new Info.plist key: {}".format(key))
self.info[key] = val
changed = True
if changed:
biplist.writePlist(self.info, self.info_path, binary=True)
else:
self.orig_info = None
def info_props_changed(self):
return self.orig_info is not None
def info_prop_changed(self, key):
if not self.orig_info:
# No props have been changed
return False
if key in self.info and key in self.orig_info and self.info[key] == self.orig_info[key]:
return False
return True
def get_info_prop(self, key):
return self.info[key]
def sign_dylibs(self, signer, path):
""" Sign all the dylibs in this directory """
for dylib_path in glob.glob(join(path, '*.dylib')):
dylib = signable.Dylib(self, dylib_path, signer)
dylib.sign(self, signer)
def sign(self, signer):
""" Sign everything in this bundle, recursively with sub-bundles """
# log.debug("SIGNING: %s" % self.path)
frameworks_path = join(self.path, 'Frameworks')
if exists(frameworks_path):
# log.debug("SIGNING FRAMEWORKS: %s" % frameworks_path)
# sign all the frameworks
for framework_name in os.listdir(frameworks_path):
framework_path = join(frameworks_path, framework_name)
# log.debug("checking for framework: %s" % framework_path)
try:
framework = Framework(framework_path)
# log.debug("resigning: %s" % framework_path)
framework.resign(signer)
except NotMatched:
# log.debug("not a framework: %s" % framework_path)
continue
# sign all the dylibs under Frameworks
self.sign_dylibs(signer, frameworks_path)
# sign any dylibs in the main directory (rare, but it happens)
self.sign_dylibs(signer, self.path)
plugins_path = join(self.path, 'PlugIns')
if exists(plugins_path):
# sign the appex executables
appex_paths = glob.glob(join(plugins_path, '*.appex'))
for appex_path in appex_paths:
plist_path = join(appex_path, 'Info.plist')
if not exists(plist_path):
continue
plist = biplist.readPlist(plist_path)
appex_exec_path = join(appex_path, plist['CFBundleExecutable'])
appex = signable.Appex(self, appex_exec_path, signer)
appex.sign(self, signer)
# then create the seal
# TODO maybe the app should know what its seal path should be...
self.seal_path = code_resources.make_seal(self.get_executable_path(),
self.path)
# then sign the app
executable = self.signable_class(self, self.get_executable_path(), signer)
executable.sign(self, signer)
def resign(self, signer):
""" signs bundle, modifies in place """
self.sign(signer)
log.debug("Resigned bundle at <%s>", self.path)
class Framework(Bundle):
""" A bundle that comprises reusable code. Similar to an app in that it has
its own resources and metadata. Not like an app because the main executable
doesn't have Entitlements, or an Application hash, and it doesn't have its
own provisioning profile. """
# the executable in this bundle will be a Framework
signable_class = signable.Framework
def __init__(self, path):
super(Framework, self).__init__(path)
class App(Bundle):
""" The kind of bundle that is visible as an app to the user.
Contains the provisioning profile, entitlements, etc. """
# the executable in this bundle will be an Executable (i.e. the main
# executable of an app)
signable_class = signable.Executable
def __init__(self, path):
super(App, self).__init__(path)
self.entitlements_path = join(self.path,
'Entitlements.plist')
self.provision_path = join(self.path,
'embedded.mobileprovision')
def provision(self, provision_path):
shutil.copyfile(provision_path, self.provision_path)
@staticmethod
def extract_entitlements(provision_path):
""" Given a path to a provisioning profile, return the entitlements
encoded therein """
cmd = [
'smime',
'-inform', 'der',
'-verify', # verifies content, prints verification status to STDERR,
# outputs content to STDOUT. In our case, will be an XML plist
'-noverify', # accept self-signed certs. Not the opposite of -verify!
'-in', provision_path
]
# this command always prints 'Verification successful' to stderr.
(profile_text, err) = openssl_command(cmd, data=None, expect_err=True)
if err and err.strip() != 'Verification successful':
log.error('Received unexpected error from openssl: {}'.format(err))
plist_dict = biplist.readPlistFromString(profile_text)
if 'Entitlements' not in plist_dict:
log.debug('failed to get entitlements in provisioning profile')
raise Exception('could not find Entitlements in {}'.format(provision_path))
return plist_dict['Entitlements']
def write_entitlements(self, entitlements):
""" Write entitlements to self.entitlements_path. This actually doesn't matter
to the app, it's just used later on by other parts of the signing process. """
biplist.writePlist(entitlements, self.entitlements_path, binary=False)
log.debug("wrote Entitlements to {0}".format(self.entitlements_path))
def resign(self, signer, provisioning_profile, alternate_entitlements_path=None):
""" signs app in place """
# TODO all this mucking about with entitlements feels wrong. The entitlements_path is
# not actually functional, it's just a way of passing it to later stages of signing.
# Maybe we should determine entitlements data in isign/archive.py or even isign/isign.py,
# and then embed it into Signer?
# In the typical case, we add entitlements from the pprof into the app's signature
if alternate_entitlements_path is None:
# copy the provisioning profile in
self.provision(provisioning_profile)
entitlements = self.extract_entitlements(provisioning_profile)
else:
log.info("signing with alternative entitlements: {}".format(alternate_entitlements_path))
entitlements = biplist.readPlist(alternate_entitlements_path)
self.write_entitlements(entitlements)
# actually resign this bundle now
super(App, self).resign(signer)
================================================
FILE: isign/code_resources.py
================================================
# -*- coding: utf-8 -*-
import binascii
import copy
import hashlib
import logging
from memoizer import memoize
import os
import plistlib
from plistlib import PlistWriter
import re
OUTPUT_DIRECTORY = '_CodeSignature'
OUTPUT_FILENAME = 'CodeResources'
TEMPLATE_FILENAME = 'code_resources_template.xml'
# DIGEST_ALGORITHM = "sha1"
HASH_BLOCKSIZE = 65536
log = logging.getLogger(__name__)
# have to monkey patch Plist, in order to make the values
# look the same - no .0 for floats
# Apple's plist utils work like this:
# 1234.5 ---> <real>1234.5</real>
# 1234.0 ---> <real>1234</real>
def writeValue(self, value):
if isinstance(value, float):
rep = repr(value)
if value.is_integer():
rep = repr(int(value))
self.simpleElement("real", rep)
else:
self.oldWriteValue(value)
PlistWriter.oldWriteValue = PlistWriter.writeValue
PlistWriter.writeValue = writeValue
# Simple reimplementation of ResourceBuilder, in the Apple Open Source
# file bundlediskrep.cpp
class PathRule(object):
OPTIONAL = 0x01
OMITTED = 0x02
NESTED = 0x04
EXCLUSION = 0x10 # unused?
TOP = 0x20 # unused?
def __init__(self, pattern='', properties=None):
# on Mac OS the FS is case-insensitive; simulate that here
self.pattern = re.compile(pattern, re.IGNORECASE)
self.flags = 0
self.weight = 0
if properties is not None:
if type(properties) == 'bool':
if properties is False:
self.flags |= PathRule.OMITTED
# if it was true, this file is required;
# do nothing
elif isinstance(properties, dict):
for key, value in properties.iteritems():
if key == 'optional' and value is True:
self.flags |= PathRule.OPTIONAL
elif key == 'omit' and value is True:
self.flags |= PathRule.OMITTED
elif key == 'nested' and value is True:
self.flags |= PathRule.NESTED
elif key == 'weight':
self.weight = float(value)
def is_optional(self):
return self.flags & PathRule.OPTIONAL != 0
def is_omitted(self):
return self.flags & PathRule.OMITTED != 0
def is_nested(self):
return self.flags & PathRule.NESTED != 0
def is_exclusion(self):
return self.flags & PathRule.EXCLUSION != 0
def is_top(self):
return self.flags & PathRule.TOP != 0
def matches(self, path):
return re.match(self.pattern, path)
def __str__(self):
return 'PathRule:' + str(self.flags) + ':' + str(self.weight)
class ResourceBuilder(object):
NULL_PATH_RULE = PathRule()
def __init__(self, app_path, rules_data, respect_omissions=False, include_sha256=False):
self.app_path = app_path
self.app_dir = os.path.dirname(app_path)
self.rules = []
self.respect_omissions = respect_omissions
self.include_sha256 = include_sha256
for pattern, properties in rules_data.iteritems():
self.rules.append(PathRule(pattern, properties))
def find_rule(self, path):
best_rule = ResourceBuilder.NULL_PATH_RULE
for rule in self.rules:
# log.debug('trying rule ' + str(rule) + ' against ' + path)
if rule.matches(path):
if rule.flags and rule.is_exclusion():
best_rule = rule
break
elif rule.weight > best_rule.weight:
best_rule = rule
return best_rule
def get_rule_and_paths(self, root, path):
path = os.path.join(root, path)
relative_path = os.path.relpath(path, self.app_dir)
rule = self.find_rule(relative_path)
return (rule, path, relative_path)
def scan(self):
"""
Walk entire directory, compile mapping
path relative to source_dir -> digest and other data
"""
file_entries = {}
# rule_debug_fmt = "rule: {0}, path: {1}, relative_path: {2}"
for root, dirs, filenames in os.walk(self.app_dir):
# log.debug("root: {0}".format(root))
for filename in filenames:
rule, path, relative_path = self.get_rule_and_paths(root,
filename)
# log.debug(rule_debug_fmt.format(rule, path, relative_path))
# specifically ignore the CodeResources symlink in base directory if it exists (iOS 11+ fix)
if relative_path == "CodeResources" and os.path.islink(path):
continue
if rule.is_exclusion():
continue
if rule.is_omitted() and self.respect_omissions is True:
continue
if self.app_path == path:
continue
# in the case of symlinks, we don't calculate the hash but rather add a key for it being a symlink
if os.path.islink(path):
# omit symlinks from files, leave in files2
if not self.respect_omissions:
continue
val = {'symlink': os.readlink(path)}
else:
# the Data element in plists is base64-encoded
val = {'hash': plistlib.Data(get_hash_binary(path))}
if self.include_sha256:
val['hash2'] = plistlib.Data(get_hash_binary(path, 'sha256'))
if rule.is_optional():
val['optional'] = True
if len(val) == 1 and 'hash' in val:
file_entries[relative_path] = val['hash']
else:
file_entries[relative_path] = val
for dirname in dirs:
rule, path, relative_path = self.get_rule_and_paths(root,
dirname)
if rule.is_nested() and '.' not in path:
dirs.remove(dirname)
continue
if relative_path == OUTPUT_DIRECTORY:
dirs.remove(dirname)
return file_entries
def get_template():
"""
Obtain the 'template' plist which also contains things like
default rules about which files should count
"""
current_dir = os.path.dirname(os.path.abspath(__file__))
template_path = os.path.join(current_dir, TEMPLATE_FILENAME)
fh = open(template_path, 'r')
return plistlib.readPlist(fh)
@memoize
def get_hash_hex(path, hash_type='sha1'):
""" Get the hash of a file at path, encoded as hexadecimal """
if hash_type == 'sha256':
hasher = hashlib.sha256()
elif hash_type == 'sha1':
hasher = hashlib.sha1()
else:
raise ValueError("Incorrect hash type provided: {}".format(hash_type))
with open(path, 'rb') as afile:
buf = afile.read(HASH_BLOCKSIZE)
while len(buf) > 0:
hasher.update(buf)
buf = afile.read(HASH_BLOCKSIZE)
return hasher.hexdigest()
@memoize
def get_hash_binary(path, hash_type='sha1'):
""" Get the hash of a file at path, encoded as binary """
return binascii.a2b_hex(get_hash_hex(path, hash_type))
def write_plist(target_dir, plist):
""" Write the CodeResources file """
output_dir = os.path.join(target_dir, OUTPUT_DIRECTORY)
if not os.path.exists(output_dir):
os.makedirs(output_dir)
output_path = os.path.join(output_dir, OUTPUT_FILENAME)
fh = open(output_path, 'w')
plistlib.writePlist(plist, fh)
return output_path
def make_seal(source_app_path, target_dir=None):
"""
Given a source app, create a CodeResources file for the
surrounding directory, and write it into the appropriate path in a target
directory
"""
if target_dir is None:
target_dir = os.path.dirname(source_app_path)
template = get_template()
# n.b. code_resources_template not only contains a template of
# what the file should look like; it contains default rules
# deciding which files should be part of the seal
rules = template['rules']
plist = copy.deepcopy(template)
resource_builder = ResourceBuilder(source_app_path, rules, respect_omissions=False)
plist['files'] = resource_builder.scan()
rules2 = template['rules2']
resource_builder2 = ResourceBuilder(source_app_path, rules2, respect_omissions=True, include_sha256=True)
plist['files2'] = resource_builder2.scan()
return write_plist(target_dir, plist)
================================================
FILE: isign/code_resources_template.xml
================================================
<?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>
<key>files</key>
<dict>
</dict>
<key>rules</key>
<dict>
<key>^.*</key>
<true/>
<key>^.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^version.plist$</key>
<true/>
</dict>
<key>rules2</key>
<dict>
<key>.*\.dSYM($|/)</key>
<dict>
<key>weight</key>
<real>11</real>
</dict>
<key>^(.*/)?\.DS_Store$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>2000</real>
</dict>
<key>^.*</key>
<true/>
<key>^.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^Info\.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^PkgInfo$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^embedded\.provisionprofile$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^version\.plist$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
</dict>
</dict>
</plist>
================================================
FILE: isign/codesig.py
================================================
# -*- coding: utf-8 -*-
from abc import ABCMeta
import construct
import hashlib
import logging
import macho_cs
import utils
log = logging.getLogger(__name__)
# See the documentation for an explanation of how
# CodeDirectory slots work.
class CodeDirectorySlot(object):
__metaclass__ = ABCMeta
offset = None
def __init__(self, codesig):
self.codesig = codesig
def get_hash(self, hash_algorithm):
if hash_algorithm == "sha1":
return hashlib.sha1(self.get_contents()).digest()
elif hash_algorithm == "sha256":
return hashlib.sha256(self.get_contents()).digest()
else:
raise ValueError('Incorrect hash algorith {}'.format(hash_algorithm))
class DerEntitlementsSlot(CodeDirectorySlot):
offset = -7
def get_contents(self):
blobs = self.codesig.get_blobs('CSMAGIC_DER_ENTITLEMENT', min_expected=1, max_expected=1)
return self.codesig.get_blob_data(blobs[0])
class EntitlementsSlot(CodeDirectorySlot):
offset = -5
def get_contents(self):
blobs = self.codesig.get_blobs('CSMAGIC_ENTITLEMENT', min_expected=1, max_expected=1)
return self.codesig.get_blob_data(blobs[0])
class ApplicationSlot(CodeDirectorySlot):
offset = -4
def get_hash(self, hash_algorithm):
if hash_algorithm == "sha1":
hash_length = 20
elif hash_algorithm == "sha256":
hash_length = 32
else:
raise ValueError('Incorrect hash algorith {}'.format(hash_algorithm))
return '\x00' * hash_length
class ResourceDirSlot(CodeDirectorySlot):
offset = -3
def __init__(self, seal_path):
self.seal_path = seal_path
def get_contents(self):
return open(self.seal_path, "rb").read()
class RequirementsSlot(CodeDirectorySlot):
offset = -2
def get_contents(self):
blobs = self.codesig.get_blobs('CSMAGIC_REQUIREMENTS', min_expected=1, max_expected=1)
return self.codesig.get_blob_data(blobs[0])
class InfoSlot(CodeDirectorySlot):
offset = -1
def __init__(self, info_path):
self.info_path = info_path
def get_contents(self):
return open(self.info_path, "rb").read()
# Represents a code signature object, aka the LC_CODE_SIGNATURE,
# within the Signable
class Codesig(object):
""" wrapper around construct for code signature """
def __init__(self, signable, data):
self.signable = signable
self.construct = macho_cs.Blob.parse(data)
self.is_sha256 = len(self.construct.data.BlobIndex) >= 6
def is_sha256_signature(self):
return self.is_sha256
def build_data(self):
return macho_cs.Blob.build(self.construct)
def get_blobs(self, magic, min_expected=None, max_expected=None):
""" get the blobs corresponding to the magic value from the blob index """
blobs = []
for index in self.construct.data.BlobIndex:
if index.blob.magic == magic:
blobs.append(index.blob)
if min_expected != None and len(blobs) < min_expected:
raise KeyError("""The number of slots in blob index for magic '{}' was less than
the minimum expected ({})""".format(magic, min_expected))
if max_expected != None and len(blobs) > max_expected:
raise KeyError("""The number of slots in blob index for magic '{}' was more than
the maximum expected ({})""".format(magic, max_expected))
return blobs
def get_blob_data(self, blob):
""" convenience method, if we just want the data """
return macho_cs.Blob_.build(blob)
def set_entitlements(self, entitlements_path):
# log.debug("entitlements:")
try:
entitlements_blobs = self.get_blobs('CSMAGIC_ENTITLEMENT', min_expected=1, max_expected=1)
entitlements = entitlements_blobs[0]
# log.debug("found entitlements slot in the image")
except KeyError:
# log.debug("no entitlements found")
pass
else:
# make entitlements data if slot was found
# libraries do not have entitlements data
# so this is actually a difference between libs and apps
# entitlements_data = macho_cs.Blob_.build(entitlements)
# log.debug(hashlib.sha1(entitlements_data).hexdigest())
log.debug("using entitlements at path: {}".format(entitlements_path))
entitlements.bytes = open(entitlements_path, "rb").read()
entitlements.length = len(entitlements.bytes) + 8
def set_requirements(self, signer):
# log.debug("requirements:")
requirements_blobs = self.get_blobs('CSMAGIC_REQUIREMENTS', min_expected=1, max_expected=1)
requirements = requirements_blobs[0]
# requirements_data = macho_cs.Blob_.build(requirements)
# log.debug(hashlib.sha1(requirements_data).hexdigest())
signer_cn = signer.get_common_name()
# this is for convenience, a reference to the first blob
# structure within requirements, which contains the data
# we are going to change
req_blob_0 = requirements.data.BlobIndex[0].blob
req_blob_0_original_length = req_blob_0.length
if self.signable.get_changed_bundle_id():
# Set the bundle id if it changed
try:
bundle_struct = req_blob_0.data.expr.data[0].data
bundle_struct.data = self.signable.get_changed_bundle_id()
bundle_struct.length = len(bundle_struct.data)
except Exception:
log.debug("could not set bundle id")
try:
cn = req_blob_0.data.expr.data[1].data[1].data[0].data[2].Data
except Exception:
log.debug("no signer CN rule found in requirements")
log.debug(requirements)
else:
# if we could find a signer CN rule, make requirements.
# first, replace old signer CN with our own
cn.data = signer_cn
cn.length = len(cn.data)
# req_blob_0 contains that CN, so rebuild it, and get what
# the length is now
req_blob_0.bytes = macho_cs.Requirement.build(req_blob_0.data)
req_blob_0.length = len(req_blob_0.bytes) + 8
# fix offsets of later blobs in requirements
offset_delta = req_blob_0.length - req_blob_0_original_length
for bi in requirements.data.BlobIndex[1:]:
bi.offset += offset_delta
# rebuild requirements, and set length for whole thing
requirements.bytes = macho_cs.Entitlements.build(requirements.data)
requirements.length = len(requirements.bytes) + 8
# then rebuild the whole data, but just to show the digest...?
# requirements_data = macho_cs.Blob_.build(requirements)
# log.debug(hashlib.sha1(requirements_data).hexdigest())
def get_codedirectory_hash_index(self, slot, code_directory):
""" The slots have negative offsets, because they start from the 'top'.
So to get the actual index, we add it to the length of the
slots. """
return slot.offset + code_directory.data.nSpecialSlots
def has_codedirectory_slot(self, slot, code_directory):
""" Some dylibs have all 5 slots, even though technically they only need
the first 2. If this dylib only has 2 slots, some of the calculated
indices for slots will be negative. This means we don't do
those slots when resigning (for dylibs, they don't add any
security anyway) """
return self.get_codedirectory_hash_index(slot, code_directory) >= 0
def fill_codedirectory_slot(self, slot, code_directory, hash_algorithm):
if self.signable.should_fill_slot(self, slot):
index = self.get_codedirectory_hash_index(slot, code_directory)
code_directory.data.hashes[index] = slot.get_hash(hash_algorithm)
def set_codedirectories(self, seal_path, info_path, signer):
cd = self.get_blobs('CSMAGIC_CODEDIRECTORY', min_expected=1, max_expected=2)
changed_bundle_id = self.signable.get_changed_bundle_id()
hash_size_sha_mapping = { 32: 'sha256', 20: 'sha1' }
for i, code_directory in enumerate(cd):
hash_algorithm = hash_size_sha_mapping.get(code_directory.data.hashSize)
if self.has_codedirectory_slot(EntitlementsSlot, code_directory):
self.fill_codedirectory_slot(EntitlementsSlot(self), code_directory, hash_algorithm)
if self.has_codedirectory_slot(ResourceDirSlot, code_directory):
self.fill_codedirectory_slot(ResourceDirSlot(seal_path), code_directory, hash_algorithm)
if self.has_codedirectory_slot(RequirementsSlot, code_directory):
self.fill_codedirectory_slot(RequirementsSlot(self), code_directory, hash_algorithm)
if self.has_codedirectory_slot(ApplicationSlot, code_directory):
self.fill_codedirectory_slot(ApplicationSlot(self), code_directory, hash_algorithm)
if self.has_codedirectory_slot(InfoSlot, code_directory):
self.fill_codedirectory_slot(InfoSlot(info_path), code_directory, hash_algorithm)
if self.has_codedirectory_slot(DerEntitlementsSlot, code_directory):
self.fill_codedirectory_slot(DerEntitlementsSlot(self), code_directory, hash_algorithm)
code_directory.data.teamID = signer.team_id
if changed_bundle_id:
offset_change = len(changed_bundle_id) - len(code_directory.data.ident)
code_directory.data.ident = changed_bundle_id
code_directory.data.hashOffset += offset_change
if code_directory.data.teamIDOffset == None:
code_directory.data.teamIDOffset = offset_change
else:
code_directory.data.teamIDOffset += offset_change
code_directory.length += offset_change
code_directory.bytes = macho_cs.CodeDirectory.build(code_directory.data)
# cd_data = macho_cs.Blob_.build(cd)
# log.debug(len(cd_data))
# open("cdrip", "wb").write(cd_data)
# log.debug("CDHash:" + hashlib.sha1(cd_data).hexdigest())
def set_signature(self, signer):
# TODO how do we even know this blobwrapper contains the signature?
# seems like this is a coincidence of the structure, where
# it's the only blobwrapper at that level...
# log.debug("sig:")
blob_wrappers = self.get_blobs('CSMAGIC_BLOBWRAPPER', min_expected=1, max_expected=1)
sigwrapper = blob_wrappers[0]
# oldsig = sigwrapper.bytes.value
# signer._log_parsed_asn1(sigwrapper.data.data.value)
# open("sigrip.der", "wb").write(sigwrapper.data.data.value)
code_directories = self.get_blobs('CSMAGIC_CODEDIRECTORY', min_expected=1, max_expected=2)
cd_data = self.get_blob_data(code_directories[0])
sig = signer.sign(cd_data, 'sha1')
# log.debug("sig len: {0}".format(len(sig)))
# log.debug("old sig len: {0}".format(len(oldsig)))
# open("my_sigrip.der", "wb").write(sig)
sigwrapper.data = construct.Container(data=sig)
# signer._log_parsed_asn1(sig)
# sigwrapper.data = construct.Container(data="hahaha")
sigwrapper.length = len(sigwrapper.data.data) + 8
sigwrapper.bytes = sigwrapper.data.data
# log.debug(len(sigwrapper.bytes))
def update_offsets(self):
# update section offsets, to account for any length changes
offset = self.construct.data.BlobIndex[0].offset
for blob in self.construct.data.BlobIndex:
blob.offset = offset
blob_data = macho_cs.Blob.build(blob.blob)
offset += len(blob_data)
superblob = macho_cs.SuperBlob.build(self.construct.data)
self.construct.length = len(superblob) + 8
self.construct.bytes = superblob
def resign(self, bundle, signer):
""" Do the actual signing. Create the structre and then update all the
byte offsets """
codedirs = self.get_blobs('CSMAGIC_CODEDIRECTORY', min_expected=1, max_expected=2)
# TODO - the way entitlements are handled is a code smell
# 1 - We're doing a hasattr to detect whether it's a top-level app. isinstance(App, bundle) ?
# 2 - unlike the seal_path and info_path, the entitlements_path is not functional. Apps are verified
# based on the entitlements encoded into the code signature and slots and MAYBE the pprof.
# Possible refactor - make entitlements data part of Signer rather than Bundle?
if hasattr(bundle, 'entitlements_path') and bundle.entitlements_path is not None:
self.set_entitlements(bundle.entitlements_path)
self.set_requirements(signer)
# See docs/codedirectory.rst for some notes on optional hashes
self.set_codedirectories(bundle.seal_path, bundle.info_path, signer)
self.set_signature(signer)
self.update_offsets()
# TODO make this optional, in case we want to check hashes or something
# log.debug(hashes)
# cd = codesig_cons.data.BlobIndex[0].blob.data
# end_offset = arch_macho.macho_start + cd.codeLimit
# start_offset = ((end_offset + 0xfff) & ~0xfff) - (cd.nCodeSlots * 0x1000)
# for i in xrange(cd.nSpecialSlots):
# expected = cd.hashes[i]
# log.debug("special exp=%s" % expected.encode('hex'))
# for i in xrange(cd.nCodeSlots):
# expected = cd.hashes[cd.nSpecialSlots + i]
# f.seek(start_offset + 0x1000 * i)
# actual_data = f.read(min(0x1000, end_offset - f.tell()))
# actual = hashlib.sha1(actual_data).digest()
# log.debug('[%s] exp=%s act=%s' % ()
# ('bad', 'ok ')[expected == actual],
# expected.encode('hex'),
# actual.encode('hex')
# )
================================================
FILE: isign/der_encoder.py
================================================
import six
from pyasn1.codec.der import encoder
from pyasn1.type import univ
from pyasn1.type import char
def encode(element):
der_object = _turn_into_der_structure(element)
return encoder.encode(der_object)
def _turn_into_der_structure(element):
if isinstance(element, dict):
set_element = univ.SetOf()
for set_index, (element_key,element_value) in enumerate(six.iteritems(element)):
entry_sequence = univ.Sequence()
entry_sequence[0] = _turn_into_der_structure(element_key)
entry_sequence[1] = _turn_into_der_structure(element_value)
set_element[set_index] = entry_sequence
return set_element
elif isinstance(element, list):
sequence_element = univ.Sequence()
for sequence_index, element_item in enumerate(element):
sequence_element[sequence_index] = _turn_into_der_structure(element_item)
return sequence_element
elif isinstance(element, bool):
return univ.Boolean(element)
elif isinstance(element, int):
return univ.Integer(element)
elif isinstance(element, str):
return char.UTF8String(element)
elif isinstance(element, bytes):
return univ.OctetString(element)
elif element is None:
return univ.Null()
else:
raise ValueError('Unsupported type for DER: {}'.format(type(element)))
================================================
FILE: isign/exceptions.py
================================================
# -*- coding: utf-8 -*-
""" Some common exceptions """
class NotSignable(Exception):
""" superclass for any reason why app shouldn't be
signable """
pass
class NotMatched(Exception):
""" thrown if we can't find any app class for
this file path """
pass
class MissingHelpers(NotSignable):
""" thrown if helper apps are missing """
pass
class MissingCredentials(Exception):
""" thrown if credentials are missing """
pass
class ImproperCredentials(Exception):
""" thrown if something looks fishy about credentials """
pass
class OpenSslFailure(Exception):
""" something is wrong with openssl output """
pass
================================================
FILE: isign/isign.py
================================================
# -*- coding: utf-8 -*-
import archive
# import makesig
import exceptions
import os
from os.path import dirname, exists, join, realpath
# this comes with the repo
PACKAGE_ROOT = dirname(realpath(__file__))
DEFAULT_APPLE_CERT_PATH = join(PACKAGE_ROOT, 'apple_credentials', 'applecerts.pem')
DEFAULT_CREDENTIAL_FILE_NAMES = {
'certificate': 'certificate.pem',
'key': 'key.pem',
'provisioning_profile': 'isign.mobileprovision'
}
class NotSignable(Exception):
""" This is just so we don't expose other sorts of exceptions """
pass
def get_credential_paths(directory, file_names=DEFAULT_CREDENTIAL_FILE_NAMES):
""" Given a directory, return dict of paths to standard
credential files """
paths = {}
for (k, file_name) in file_names.iteritems():
paths[k] = join(directory, file_name)
return paths
# We will default to using credentials in a particular
# directory with well-known names. This is complicated because
# the old way at Sauce Labs (pre-2017) was:
# ~/isign-credentials/mobdev.cert.pem, etc.
# But the new way that everyone should now use:
# ~/.isign/certificate.pem, etc.
if exists(join(os.environ['HOME'], 'isign-credentials')):
DEFAULT_CREDENTIAL_PATHS = get_credential_paths(
join(os.environ['HOME'], 'isign-credentials'),
{
'certificate': 'mobdev.cert.pem',
'key': 'mobdev.key.pem',
'provisioning_profile': 'mobdev1.mobileprovision'
}
)
else:
DEFAULT_CREDENTIAL_PATHS = get_credential_paths(
join(os.environ['HOME'], '.isign')
)
def resign_with_creds_dir(input_path,
credentials_directory,
**kwargs):
""" Do isign.resign(), but with credential files from this directory """
kwargs.update(get_credential_paths(credentials_directory))
return resign(input_path, **kwargs)
def resign(input_path,
apple_cert=DEFAULT_APPLE_CERT_PATH,
certificate=DEFAULT_CREDENTIAL_PATHS['certificate'],
key=DEFAULT_CREDENTIAL_PATHS['key'],
provisioning_profile=DEFAULT_CREDENTIAL_PATHS['provisioning_profile'],
output_path=join(os.getcwd(), "out"),
info_props=None,
alternate_entitlements_path=None):
""" Mirrors archive.resign(), put here for convenience, to unify exceptions,
and to omit default args """
try:
return archive.resign(input_path,
certificate,
key,
apple_cert,
provisioning_profile,
output_path,
info_props,
alternate_entitlements_path)
except exceptions.NotSignable as e:
# re-raise the exception without exposing internal
# details of how it happened
raise NotSignable(e)
def view(input_path):
""" Obtain information about the app """
try:
return archive.view(input_path)
except exceptions.NotSignable as e:
raise NotSignable(e)
================================================
FILE: isign/macho.py
================================================
# -*- coding: utf-8 -*-
#
# Constructs to represent various structures
# in a Mach-O binary.
#
# As with all Constructs, can be used for both
# parsing or emitting (aka building)
#
from construct import *
from macho_cs import Blob
UInt32 = ULInt32
UInt64 = ULInt64
CodeSigRef = Struct("codesig",
UInt32('dataoff'),
UInt32('datasize'),
)
Segment = Struct('segment',
PaddedStringAdapter(Bytes('segname', 16)),
UInt32('vmaddr'),
UInt32('vmsize'),
UInt32('fileoff'),
UInt32('filesize'),
UInt32('maxprot'),
UInt32('initprot'),
UInt32('nsects'),
UInt32('flags'),
Rename("sections",
Array(lambda ctx: ctx['nsects'],
Struct('Section',
PaddedStringAdapter(Bytes('sectname', 16)),
PaddedStringAdapter(Bytes('segname', 16)),
UInt64('addr'),
UInt64('size'),
UInt32('offset'),
UInt32('align'),
UInt32('reloff'),
UInt32('nreloc'),
UInt32('flags'),
UInt32('reserved1'),
UInt32('reserved2'),
UInt32('reserved3'),
))),
)
Segment64 = Struct('segment',
PaddedStringAdapter(Bytes('segname', 16)),
UInt64('vmaddr'),
UInt64('vmsize'),
UInt64('fileoff'),
UInt64('filesize'),
UInt32('maxprot'),
UInt32('initprot'),
UInt32('nsects'),
UInt32('flags'),
Rename("sections",
Array(lambda ctx: ctx['nsects'],
Struct('Section',
PaddedStringAdapter(Bytes('sectname', 16)),
PaddedStringAdapter(Bytes('segname', 16)),
UInt64('addr'),
UInt64('size'),
UInt32('offset'),
UInt32('align'),
UInt32('reloff'),
UInt32('nreloc'),
UInt32('flags'),
UInt32('reserved1'),
UInt32('reserved2'),
UInt32('reserved3'),
))),
)
LoadCommand = Struct("LoadCommand",
Enum(UInt32("cmd"),
LC_SEGMENT=0x1,
LC_SYMTAB=0x2,
LC_SYMSEG=0x3,
LC_THREAD=0x4,
LC_UNIXTHREAD=0x5,
LC_LOADFVMLIB=0x6,
LC_IDFVMLIB=0x7,
LC_IDENT=0x8,
LC_FVMFILE=0x9,
LC_PREPAGE=0xa,
LC_DYSYMTAB=0xb,
LC_LOAD_DYLIB=0xc,
LC_ID_DYLIB=0xd,
LC_LOAD_DYLINKER=0xe,
LC_ID_DYLINKER=0xf,
LC_PREBOUND_DYLIB=0x10,
LC_ROUTINES=0x11,
LC_SUB_FRAMEWORK=0x12,
LC_SUB_UMBRELLA=0x13,
LC_SUB_CLIENT=0x14,
LC_SUB_LIBRARY=0x15,
LC_TWOLEVEL_HINTS=0x16,
LC_PREBIND_CKSUM=0x17,
LC_LOAD_WEAK_DYLIB=0x80000018,
LC_SEGMENT_64=0x19,
LC_ROUTINES_64=0x1a,
LC_UUID=0x1b,
LC_RPATH=0x8000001c,
LC_CODE_SIGNATURE=0x1d,
LC_SEGMENT_SPLIT_INFO=0x1e,
LC_REEXPORT_DYLIB=0x8000001f,
LC_LAZY_LOAD_DYLIB=0x20,
LC_ENCRYPTION_INFO=0x21,
LC_DYLD_INFO=0x22,
LC_DYLD_INFO_ONLY=0x80000022,
LC_LOAD_UPWARD_DYLIB=0x80000023,
LC_VERSION_MIN_MACOSX=0x24,
LC_VERSION_MIN_IPHONEOS=0x25,
LC_FUNCTION_STARTS=0x26,
LC_DYLD_ENVIRONMENT=0x27,
LC_MAIN=0x80000028,
LC_DATA_IN_CODE=0x29,
LC_SOURCE_VERSION=0x2a,
LC_DYLIB_CODE_SIGN_DRS=0x2b,
LC_ENCRYPTION_INFO_64=0x2c,
LC_LINKER_OPTION=0x2d,
LC_LINKER_OPTIMIZATION_HINT=0x2e,
LC_VERSION_MIN_TVOS=0x2f,
LC_VERSION_MIN_WATCHOS=0x30,
LC_NOTE=0x31,
LC_BUILD_VERSION=0x32,
LC_DYLD_EXPORTS_TRIE = 0x80000033,
LC_DYLD_CHAINED_FIXUPS = 0x80000034,
LC_FILESET_ENTRY = 0x80000035
),
UInt32("cmdsize"),
Peek(Switch("data", lambda ctx: ctx['cmd'],
{'LC_SEGMENT': Struct('segment',
PaddedStringAdapter(Bytes('segname', 16)),
UInt32('vmaddr'),
UInt32('vmsize'),
UInt32('fileoff'),
UInt32('filesize'),
UInt32('maxprot'),
UInt32('initprot'),
UInt32('nsects'),
UInt32('flags'),
Rename("sections",
Array(lambda ctx: ctx['nsects'],
Struct('Section',
PaddedStringAdapter(Bytes('sectname', 16)),
PaddedStringAdapter(Bytes('segname', 16)),
UInt32('addr'),
UInt32('size'),
UInt32('offset'),
UInt32('align'),
UInt32('reloff'),
UInt32('nreloc'),
UInt32('flags'),
UInt32('reserved1'),
UInt32('reserved2'),
))),
),
'LC_SEGMENT_64': Segment64,
'LC_DYLIB_CODE_SIGN_DRS': Struct("codesign_drs",
UInt32('dataoff'),
UInt32('datasize'),
Pointer(lambda ctx: ctx['_']['_']['macho_start'] + ctx['dataoff'], Blob),
),
'LC_CODE_SIGNATURE': Struct("codesig",
UInt32('dataoff'),
UInt32('datasize'),
Pointer(lambda ctx: ctx['_']['_']['macho_start'] + ctx['dataoff'], Blob),
),
}, default=Pass)),
OnDemand(Bytes('bytes', lambda ctx: ctx['cmdsize'] - 8)),
#Probe(),
)
MachO = Struct("MachO",
Anchor("macho_start"),
Enum(UInt32("magic"),
MH_MAGIC=0xfeedface,
MH_MAGIC_64=0xfeedfacf,
MH_CIGAM_64=0xcffaedfe,
),
UInt32("cputype"),
UInt32("cpusubtype"),
Enum(UInt32("filetype"),
MH_OBJECT=0x1,
MH_EXECUTE=0x2,
MH_FVMLIB=0x3,
MH_CORE=0x4,
MH_PRELOAD=0x5,
MH_DYLIB=0x6,
MH_DYLINKER=0x7,
MH_BUNDLE=0x8,
MH_DYLIB_STUB=0x9,
MH_DSYM=0xa,
MH_KEXT_BUNDLE=0xb,
_default_=Pass,
),
UInt32("ncmds"),
UInt32("sizeofcmds"),
FlagsEnum(UInt32("flags"),
MH_NOUNDEFS=0x1,
MH_INCRLINK=0x2,
MH_DYLDLINK=0x4,
MH_BINDATLOAD=0x8,
MH_PREBOUND=0x10,
MH_SPLIT_SEGS=0x20,
MH_LAZY_INIT=0x40,
MH_TWOLEVEL=0x80,
MH_FORCE_FLAT=0x100,
MH_NOMULTIDEFS=0x200,
MH_NOFIXPREBINDING=0x400,
MH_PREBINDABLE=0x800,
MH_ALLMODSBOUND=0x1000,
MH_SUBSECTIONS_VIA_SYMBOLS=0x2000,
MH_CANONICAL=0x4000,
MH_WEAK_DEFINES=0x8000,
MH_BINDS_TO_WEAK=0x10000,
MH_ALLOW_STACK_EXECUTION=0x20000,
MH_ROOT_SAFE=0x40000,
MH_SETUID_SAFE=0x80000,
MH_NO_REEXPORTED_DYLIBS=0x100000,
MH_PIE=0x200000,
MH_DEAD_STRIPPABLE_DYLIB=0x400000,
MH_HAS_TLV_DESCRIPTORS=0x00800000,
MH_NO_HEAP_EXECUTION=0x01000000,
MH_APP_EXTENSION_SAFE=0x02000000,
MH_UNUSED_1=0x04000000,
MH_UNUSED_2=0x08000000,
MH_UNUSED_3=0x10000000,
MH_UNUSED_4=0x20000000,
MH_UNUSED_5=0x40000000,
MH_UNUSED_6=0x80000000,
),
If(lambda ctx: ctx['magic'] in ('MH_MAGIC_64', 'MH_CIGAM_64'), UInt32('reserved')), Rename('commands', Array(lambda ctx: ctx['ncmds'], LoadCommand)))
FatArch = Struct("FatArch",
UBInt32("cputype"),
UBInt32("cpusubtype"),
UBInt32("offset"),
UBInt32("size"),
UBInt32("align"),
Pointer(lambda ctx: ctx['offset'], MachO),
)
Fat = Struct("Fat",
Const(UBInt32("magic"), 0xcafebabe),
UBInt32("nfat_arch"),
Array(lambda ctx: ctx['nfat_arch'], FatArch),
)
MachoFile = Struct("MachoFile",
Peek(UInt32("magic")),
Switch("data", lambda ctx: ctx['magic'], {0xfeedface: MachO,
0xfeedfacf: MachO,
0xcffaedfe: MachO,
0xcafebabe: Fat,
0xbebafeca: Fat,
0xc10cdefa: Blob,
})
)
================================================
FILE: isign/macho_cs.py
================================================
# -*- coding: utf-8 -*-
#
# This is a Construct library which represents an
# LC_CODE_SIGNATURE structure. Like all Construct
# libraries, can be used for parsing or emitting
# (Construct calls it 'building')
#
from construct import *
import plistlib
class PlistAdapter(Adapter):
def _encode(self, obj, context):
return plistlib.writePlistToString(obj)
def _decode(self, obj, context):
return plistlib.readPlistFromString(obj)
# talk about overdesign.
# magic is in the blob struct
Expr = LazyBound("expr", lambda: Expr_)
Blob = LazyBound("blob", lambda: Blob_)
Hashes = LazyBound("hashes", lambda: Hashes_)
Hashes_ = Array(lambda ctx: ctx['nSpecialSlots'] + ctx['nCodeSlots'], Bytes("hash", lambda ctx: ctx['hashSize']))
CodeDirectory = Struct("CodeDirectory",
Anchor("cd_start"),
UBInt32("version"),
UBInt32("flags"),
UBInt32("hashOffset"),
UBInt32("identOffset"),
UBInt32("nSpecialSlots"),
UBInt32("nCodeSlots"),
UBInt32("codeLimit"),
UBInt8("hashSize"),
UBInt8("hashType"),
UBInt8("spare1"),
UBInt8("pageSize"),
UBInt32("spare2"),
Pointer(lambda ctx: ctx['cd_start'] - 8 + ctx['identOffset'], CString('ident')),
If(lambda ctx: ctx['version'] >= 0x20100, UBInt32("scatterOffset")),
If(lambda ctx: ctx['version'] >= 0x20200, UBInt32("teamIDOffset")),
If(lambda ctx: ctx['version'] >= 0x20200, Pointer(lambda ctx: ctx['cd_start'] - 8 + ctx['teamIDOffset'], CString('teamID'))),
If(lambda ctx: ctx['version'] >= 0x20300, UBInt32("spare3")),
If(lambda ctx: ctx['version'] >= 0x20300, UBInt64("codeLimit64")),
If(lambda ctx: ctx['version'] >= 0x20400,UBInt64("execSegBase")),
If(lambda ctx: ctx['version'] >= 0x20400,UBInt64("execSegLimit")),
If(lambda ctx: ctx['version'] >= 0x20400,UBInt64("execSegFlags")),
Pointer(lambda ctx: ctx['cd_start'] - 8 + ctx['hashOffset'] - ctx['hashSize'] * ctx['nSpecialSlots'], Hashes)
)
Data = Struct("Data",
UBInt32("length"),
Bytes("data", lambda ctx: ctx['length']),
Padding(lambda ctx: -ctx['length'] & 3),
)
CertSlot = Enum(UBInt32("slot"),
anchorCert=-1,
leafCert=0,
_default_=Pass,
)
Match = Struct("Match",
Enum(UBInt32("matchOp"),
matchExists=0,
matchEqual=1,
matchContains=2,
matchBeginsWith=3,
matchEndsWith=4,
matchLessThan=5,
matchGreaterThan=6,
matchLessEqual=7,
matchGreaterEqual=8,
),
If(lambda ctx: ctx['matchOp'] != 'matchExists', Data)
)
expr_args = {
'opIdent': Data,
'opAnchorHash': Sequence("AnchorHash", CertSlot, Data),
'opInfoKeyValue': Data,
'opAnd': Sequence("And", Expr, Expr),
'opOr': Sequence("Or", Expr, Expr),
'opNot': Expr,
'opCDHash': Data,
'opInfoKeyField': Sequence("InfoKeyField", Data, Match),
'opEntitlementField': Sequence("EntitlementField", Data, Match),
'opCertField': Sequence("CertField", CertSlot, Data, Match),
'opCertGeneric': Sequence("CertGeneric", CertSlot, Data, Match),
'opTrustedCert': CertSlot,
}
Expr_ = Struct("Expr",
Enum(UBInt32("op"),
opFalse=0,
opTrue=1,
opIdent=2,
opAppleAnchor=3,
opAnchorHash=4,
opInfoKeyValue=5,
opAnd=6,
opOr=7,
opCDHash=8,
opNot=9,
opInfoKeyField=10,
opCertField=11,
opTrustedCert=12,
opTrustedCerts=13,
opCertGeneric=14,
opAppleGenericAnchor=15,
opEntitlementField=16,
),
Switch("data", lambda ctx: ctx['op'],
expr_args,
default=Pass),
)
Requirement = Struct("Requirement",
Const(UBInt32("kind"), 1),
Expr,
)
Entitlement = Struct("Entitlement",
# actually a plist
PlistAdapter(Bytes("data", lambda ctx: ctx['_']['length'] - 8)),
)
DerEntitlement = Struct("DerEntitlement",
# actually a plist in binary format
Bytes("data", lambda ctx: ctx['_']['length'] - 8),
)
EntitlementsBlobIndex = Struct("BlobIndex",
Enum(UBInt32("type"),
kSecHostRequirementType=1,
kSecGuestRequirementType=2,
kSecDesignatedRequirementType=3,
kSecLibraryRequirementType=4,
),
UBInt32("offset"),
Pointer(lambda ctx: ctx['_']['sb_start'] - 8 + ctx['offset'], Blob),
)
Entitlements = Struct("Entitlements", # actually a kind of super blob
Anchor("sb_start"),
UBInt32("count"),
Array(lambda ctx: ctx['count'], EntitlementsBlobIndex),
)
BlobWrapper = Struct("BlobWrapper",
OnDemand(Bytes("data", lambda ctx: ctx['_']['length'] - 8)),
)
BlobIndex = Struct("BlobIndex",
UBInt32("type"),
UBInt32("offset"),
If(lambda ctx: ctx['offset'], Pointer(lambda ctx: ctx['_']['sb_start'] - 8 + ctx['offset'], Blob)),
)
SuperBlob = Struct("SuperBlob",
Anchor("sb_start"),
UBInt32("count"),
Array(lambda ctx: ctx['count'], BlobIndex),
)
Blob_ = Struct("Blob",
Enum(UBInt32("magic"),
CSMAGIC_REQUIREMENT=0xfade0c00,
CSMAGIC_REQUIREMENTS=0xfade0c01,
CSMAGIC_CODEDIRECTORY=0xfade0c02,
CSMAGIC_ENTITLEMENT=0xfade7171, # actually, this is kSecCodeMagicEntitlement, and not defined in the C version
CSMAGIC_DER_ENTITLEMENT=0xfade7172,
CSMAGIC_BLOBWRAPPER=0xfade0b01, # and this isn't even defined in libsecurity_codesigning; it's in _utilities
CSMAGIC_EMBEDDED_SIGNATURE=0xfade0cc0,
CSMAGIC_DETACHED_SIGNATURE=0xfade0cc1,
CSMAGIC_CODE_SIGN_DRS=0xfade0c05,
_default_=Pass,
),
UBInt32("length"),
Peek(Switch("data", lambda ctx: ctx['magic'],
{'CSMAGIC_REQUIREMENT': Requirement,
'CSMAGIC_REQUIREMENTS': Entitlements,
'CSMAGIC_CODEDIRECTORY': CodeDirectory,
'CSMAGIC_ENTITLEMENT': Entitlement,
'CSMAGIC_DER_ENTITLEMENT': DerEntitlement,
'CSMAGIC_BLOBWRAPPER': BlobWrapper,
'CSMAGIC_EMBEDDED_SIGNATURE': SuperBlob,
'CSMAGIC_DETACHED_SIGNATURE': SuperBlob,
'CSMAGIC_CODE_SIGN_DRS': SuperBlob,
})),
OnDemand(Bytes('bytes', lambda ctx: ctx['length'] - 8)),
)
================================================
FILE: isign/makesig.py
================================================
# -*- coding: utf-8 -*-
# Library to construct an LC_CODE_SIGNATURE construct
# from scratch. Does not work yet.
#
# Abandoned development May 2015 when it became clear that most
# apps that were uploaded to us would already be signed. But
# we may need this someday, so preserving here.
#
import plistlib
import io
import construct
import hashlib
import logging
import math
import macho
import macho_cs
import utils
import der_encoder
log = logging.getLogger(__name__)
def make_arg(data_type, arg):
if data_type.name == 'Data':
return construct.Container(data=arg,
length=len(arg))
elif data_type.name.lower() == 'expr':
if isinstance(arg, construct.Container):
# preserve expressions that are already containerized
return arg
return make_expr(*arg)
elif data_type.name == 'slot':
if arg == 'leafCert':
return 0
return arg
elif data_type.name == 'Match':
matchOp = arg[0]
data = None
if len(arg) > 1:
data = construct.Container(data=arg[1],
length=len(arg[1]))
return construct.Container(matchOp=matchOp, Data=data)
log.debug(data_type)
log.debug(data_type.name)
log.debug(arg)
assert 0
def make_expr(op, *args):
full_op = "op" + op
data = None
data_type = macho_cs.expr_args.get(full_op)
if isinstance(data_type, macho_cs.Sequence):
if len(data_type.subcons) == len(args):
data = [make_arg(dt, arg) for dt, arg in zip(data_type.subcons, args)]
else:
# automatically nest binary operations to accept >2 args
data = [make_arg(data_type.subcons[0], args[0]),
make_expr(op, *args[1:])]
elif data_type:
data = make_arg(data_type, args[0])
return construct.Container(op=full_op,
data=data)
def make_requirements(drs, ident, common_name):
expr = make_expr(
'And',
('Ident', ident),
('AppleGenericAnchor',),
('CertField', 'leafCert', 'subject.CN', ['matchEqual', common_name]),
('CertGeneric', 1, '*\x86H\x86\xf7cd\x06\x02\x01', ['matchExists']))
des_req = construct.Container(kind=1, expr=expr)
des_req_data = macho_cs.Requirement.build(des_req)
reqs = construct.Container(
sb_start=0,
count=1,
BlobIndex=[construct.Container(type='kSecDesignatedRequirementType',
offset=28,
blob=construct.Container(magic='CSMAGIC_REQUIREMENT',
length=len(des_req_data) + 8,
data=des_req,
bytes=des_req_data))])
if drs:
dr_exprs = []
for dr in drs.data.BlobIndex:
if dr.blob is not None:
dr_exprs.append(dr.blob.data.expr)
# make_expr expects at least 2 arguments, need to verify that we pass those in, otherwise just return
if len(dr_exprs) > 1:
expr = make_expr('Or', *dr_exprs)
lib_req = construct.Container(kind=1, expr=expr)
lib_req_data = macho_cs.Requirement.build(lib_req)
reqs.BlobIndex.append(construct.Container(type='kSecLibraryRequirementType',
offset=28 + len(des_req_data) + 8,
blob=construct.Container(magic='CSMAGIC_REQUIREMENT',
length=len(lib_req_data) + 8,
data=lib_req,
bytes=lib_req_data)))
reqs.count += 1
return reqs
def build_code_directory_blob(hash_algorithm, teamID, ident_for_signature, code_limit, hashes,
exec_segment_offset, exec_segment_limit, is_main_binary):
if hash_algorithm == 'sha1':
hash_type_value = 1
hash_size = 20
elif hash_algorithm == 'sha256':
hash_type_value = 2
hash_size = 32
else:
raise ValueError("Incorrect hash type provided: {}".format(hash_algorithm))
for hash in hashes:
if len(hash) != hash_size:
raise Exception('Incorrect hash {} for length {} ({})'.format(hash, hash_size, len(hash)))
empty_hash = "\x00" * hash_size
special_slots_length = 7
# The length of the fields in the CodeDirectory is at least these fiels which are always present.
# CD Magic (4)
# length (4)
# version (4)
# flags (4)
# hashOffset (4)
# identOffset (4)
# nSpecialSlots (4)
# nCodeSlots (4)
# codeLimit (4)
# hashSize (1)
# hashType (1)
# spare1 (1)
# pageSize (1)
# spare (4)
# scatterOffset (4)
# teamIDOffset (4)
# spare3 (4)
# codeLimit64 (8)
# execSegBase (8)
# execSegLimit (8)
# execSegFlags (8)
# which in total are 88
FIXED_FIELDS_SIZE = 88
cd = construct.Container(cd_start=None,
version=0x20400,
flags=0,
identOffset= FIXED_FIELDS_SIZE,
nSpecialSlots=special_slots_length,
nCodeSlots=len(hashes),
codeLimit=code_limit,
hashSize=hash_size,
hashType=hash_type_value,
spare1=0,
pageSize=12, # Page size is indicated as a log in base 2. The size is 0x1000 = 2 ^ 12
spare2=0,
ident=ident_for_signature,
scatterOffset=0,
teamIDOffset= FIXED_FIELDS_SIZE + len(ident_for_signature),
teamID=teamID,
hashOffset= FIXED_FIELDS_SIZE + (hash_size * special_slots_length) + len(ident_for_signature) + len(teamID),
hashes=([empty_hash] * special_slots_length) + hashes,
spare3=0,
codeLimit64=0, # 0 means fallback to codeLimit
execSegBase=exec_segment_offset,
execSegLimit=exec_segment_limit,
execSegFlags=1 if is_main_binary else 0,
)
return cd
def make_basic_codesig(entitlements_file, drs, code_limit, hashes_sha1, hashes_sha256, signer, ident,
exec_segment_offset, exec_segment_limit, is_main_binary):
common_name = signer.get_common_name()
log.debug("ident: {}".format(ident))
log.debug("codelimit: {}".format(code_limit))
teamID = signer._get_team_id() + '\x00'
ident_for_signature = ident + '\x00'
cd = build_code_directory_blob(
hash_algorithm='sha1',
teamID=teamID,
ident_for_signature=ident_for_signature,
code_limit=code_limit,
hashes=hashes_sha1,
exec_segment_offset=exec_segment_offset,
exec_segment_limit=exec_segment_limit,
is_main_binary=is_main_binary)
cd_data = macho_cs.CodeDirectory.build(cd)
# SHA256 codesignature disabled for now, as it is not correctly implemented and fails on iOS 15.1. For now we are
# just adding this switch so we can enable it again once it is properly done. At least, the current signature is not
# including a signed attribute with OID 1.2.840.113635.100.9.1 containing a plist with a "cdhashes" list (on which
# it has the base64 of the SHA1 and first 20 bytes of SHA256). That missing part is probably what is missing.
sha256_codesignature_enabled = False
# Superblob has
# magic (4)
# size (4)
# num of blobs (4)
# [blob[n], offset to n ] (4 + 4) repeated for each blob
number_of_blobs = 3
if sha256_codesignature_enabled:
number_of_blobs = 4
if entitlements_file != None:
number_of_blobs += 2
offset = 4 + 4 + 4 + (8 * number_of_blobs)
cd_index = construct.Container(type=0,
offset=offset,
blob=construct.Container(magic='CSMAGIC_CODEDIRECTORY',
length=len(cd_data) + 8,
data=cd,
bytes=cd_data,
))
offset += cd_index.blob.length
reqs_sblob = make_requirements(drs, ident, common_name)
reqs_sblob_data = macho_cs.Entitlements.build(reqs_sblob)
requirements_index = construct.Container(type=2,
offset=offset,
blob=construct.Container(magic='CSMAGIC_REQUIREMENTS',
length=len(reqs_sblob_data) + 8,
data="",
bytes=reqs_sblob_data,
))
offset += requirements_index.blob.length
entitlements_index = None
der_entitlements_index = None
if entitlements_file != None:
entitlements_bytes = open(entitlements_file, "rb").read()
entitlements_index = construct.Container(type=5,
offset=offset,
blob=construct.Container(magic='CSMAGIC_ENTITLEMENT',
length=len(entitlements_bytes) + 8,
data="",
bytes=entitlements_bytes
))
offset += entitlements_index.blob.length
xml_entitlements_dict = plistlib.readPlist(io.BytesIO(entitlements_bytes))
der_entitlements_bytes = der_encoder.encode(xml_entitlements_dict)
der_entitlements_index = construct.Container(type=7,
offset=offset,
blob=construct.Container(magic='CSMAGIC_DER_ENTITLEMENT',
length=len(der_entitlements_bytes) + 8,
data="",
bytes=der_entitlements_bytes
))
offset += der_entitlements_index.blob.length
cd_sha256_index = None
if sha256_codesignature_enabled:
cd_sha256 = build_code_directory_blob(
hash_algorithm='sha256',
teamID=teamID,
ident_for_signature=ident_for_signature,
code_limit=code_limit,
hashes=hashes_sha256,
exec_segment_offset=exec_segment_offset,
exec_segment_limit=exec_segment_limit,
is_main_binary=is_main_binary)
cd_sha256_data = macho_cs.CodeDirectory.build(cd_sha256)
cd_sha256_index = construct.Container(type=0x1000,
offset=offset,
blob=construct.Container(magic='CSMAGIC_CODEDIRECTORY',
length=len(cd_sha256_data) + 8,
data=cd_sha256,
bytes=cd_sha256_data,
))
offset += cd_sha256_index.blob.length
sigwrapper_index = construct.Container(type=65536,
offset=offset,
blob=construct.Container(magic='CSMAGIC_BLOBWRAPPER',
length=0 + 8,
data="",
bytes="",
))
indicies = filter(None, [cd_index,
requirements_index,
entitlements_index,
der_entitlements_index,
cd_sha256_index,
sigwrapper_index])
superblob = construct.Container(
sb_start=0,
count=len(indicies),
BlobIndex=indicies)
data = macho_cs.SuperBlob.build(superblob)
chunk = macho_cs.Blob.build(construct.Container(
magic="CSMAGIC_EMBEDDED_SIGNATURE",
length=len(data) + 8,
data=data,
bytes=data))
return macho_cs.Blob.parse(chunk)
def make_signature(arch_macho, arch_offset, arch_size, cmds, f, entitlements_file, codesig_data_length, signer, ident):
# NB: arch_offset is absolute in terms of file start. Everything else is relative to arch_offset!
# sign from scratch
log.debug("signing from scratch")
drs = None
drs_lc = cmds.get('LC_DYLIB_CODE_SIGN_DRS')
if drs_lc:
drs = drs_lc.data.blob
codesig_offset = utils.round_up(arch_size, 16)
# generate code hashes
log.debug("codesig offset: {}".format(codesig_offset))
codeLimit = codesig_offset
log.debug("new cL: {}".format(hex(codeLimit)))
nCodeSlots = int(math.ceil(float(codesig_offset) / 0x1000))
log.debug("new nCS: {}".format(nCodeSlots))
# generate placeholder LC_CODE_SIGNATURE (like what codesign_allocate does)
fake_hashes_sha1 = ["\x00" * 20] * nCodeSlots
fake_hashes_sha256 = ["\x00" * 32] * nCodeSlots
# Initially set to 0 (for fake signature, later on populated).
exec_segment_found = False
exec_segment_offset = 0
exec_segment_limit = 0
is_main_binary = 'MH_EXECUTE' in arch_macho.filetype
log.debug("is_main_binary: {}".format(nCodeSlots))
codesig_cons = make_basic_codesig(entitlements_file,
drs,
codeLimit,
fake_hashes_sha1,
fake_hashes_sha256,
signer,
ident,
exec_segment_offset,
exec_segment_limit,
is_main_binary)
codesig_data = macho_cs.Blob.build(codesig_cons)
cmd_data = construct.Container(dataoff=codesig_offset,
datasize=codesig_data_length)
cmd = construct.Container(cmd='LC_CODE_SIGNATURE',
cmdsize=16,
data=cmd_data,
bytes=macho.CodeSigRef.build(cmd_data))
log.debug("CS blob before: {}".format(utils.print_structure(codesig_cons, macho_cs.Blob)))
log.debug("len(codesig_data): {}".format(len(codesig_data)))
codesig_length = codesig_data_length
log.debug("codesig length: {}".format(codesig_length))
log.debug("old ncmds: {}".format(arch_macho.ncmds))
arch_macho.ncmds += 1
log.debug("new ncmds: {}".format(arch_macho.ncmds))
log.debug("old sizeofcmds: {}".format(arch_macho.sizeofcmds))
arch_macho.sizeofcmds += cmd.cmdsize
log.debug("new sizeofcmds: {}".format(arch_macho.sizeofcmds))
arch_macho.commands.append(cmd)
hashes_sha1 = []
hashes_sha256 = []
if codesig_data_length > 0:
# Patch __LINKEDIT
for lc in arch_macho.commands:
if lc.cmd == 'LC_SEGMENT_64' or lc.cmd == 'LC_SEGMENT':
if (not exec_segment_found) and lc.data.segname == '__TEXT':
# Exec segment offset and limit refer to the first text segment.
exec_segment_offset = lc.data.fileoff
exec_segment_limit = lc.data.filesize
exec_segment_found = True
log.debug('Exec segment found: Offset:{}, limit:{}'.format(exec_segment_offset, exec_segment_limit))
if lc.data.segname == '__LINKEDIT':
log.debug("found __LINKEDIT, old filesize {}, vmsize {}".format(lc.data.filesize, lc.data.vmsize))
lc.data.filesize = utils.round_up(lc.data.filesize, 16) + codesig_length
if (lc.data.filesize > lc.data.vmsize):
lc.data.vmsize = utils.round_up(lc.data.filesize, 4096)
if lc.cmd == 'LC_SEGMENT_64':
lc.bytes = macho.Segment64.build(lc.data)
else:
lc.bytes = macho.Segment.build(lc.data)
log.debug("new filesize {}, vmsize {}".format(lc.data.filesize, lc.data.vmsize))
actual_data = macho.MachO.build(arch_macho)
log.debug("actual_data length with codesig LC {}".format(len(actual_data)))
# Now seek to the start of the actual data and read until the end of the arch.
f.seek(arch_offset + len(actual_data))
bytes_to_read = codesig_offset + arch_offset - f.tell()
file_slice = f.read(bytes_to_read)
if len(file_slice) < bytes_to_read:
log.warn("expected {} bytes but got {}, zero padding.".format(bytes_to_read, len(file_slice)))
file_slice += ("\x00" * (bytes_to_read - len(file_slice)))
actual_data += file_slice
for i in xrange(nCodeSlots):
actual_data_slice = actual_data[(0x1000 * i):(0x1000 * i + 0x1000)]
actual_sha1 = hashlib.sha1(actual_data_slice).digest()
log.debug("Slot {} (File page @{} sha1): {}".format(i, hex(0x1000 * i), actual_sha1.encode('hex')))
hashes_sha1.append(actual_sha1)
actual_sha256 = hashlib.sha256(actual_data_slice).digest()
log.debug("Slot {} (File page @{} sha256): {}".format(i, hex(0x1000 * i), actual_sha256.encode('hex')))
hashes_sha256.append(actual_sha256)
else:
hashes_sha1 = fake_hashes_sha1
hashes_sha256 = fake_hashes_sha256
# Replace placeholder with real one.
codesig_cons = make_basic_codesig(entitlements_file,
drs,
codeLimit,
hashes_sha1,
hashes_sha256,
signer,
ident,
exec_segment_offset,
exec_segment_limit,
is_main_binary)
codesig_data = macho_cs.Blob.build(codesig_cons)
cmd_data = construct.Container(dataoff=codesig_offset,
datasize=len(codesig_data))
cmd = construct.Container(cmd='LC_CODE_SIGNATURE',
cmdsize=16,
data=cmd_data,
bytes=macho.CodeSigRef.build(cmd_data))
arch_macho.commands[-1] = cmd
cmds['LC_CODE_SIGNATURE'] = cmd
return codesig_data
================================================
FILE: isign/multisign.py
================================================
# -*- coding: utf-8 -*-
from os.path import isdir
import isign
from archive import archive_factory
from signer import Signer
import logging
import multiprocessing
log = logging.getLogger(__name__)
MAX_PROCESSES = multiprocessing.cpu_count()
def resign(args):
""" Given a tuple consisting of a path to an uncompressed archive,
credential directory, and desired output path, resign accordingly.
Returns a tuple of (cred_dir, path to resigned app) """
ua, cred_dir, resigned_path = args
try:
log.debug('resigning with %s %s -> %s', ua.path, cred_dir, resigned_path)
# get the credential files, create the 'signer'
credential_paths = isign.get_credential_paths(cred_dir)
signer = Signer(signer_cert_file=credential_paths['certificate'],
signer_key_file=credential_paths['key'],
apple_cert_file=isign.DEFAULT_APPLE_CERT_PATH)
# sign it (in place)
ua.bundle.resign(signer, credential_paths['provisioning_profile'])
log.debug("outputing %s", resigned_path)
# and archive it there
ua.archive(resigned_path)
finally:
ua.remove()
return (cred_dir, resigned_path)
def clone_ua(args):
original_ua, target_ua_path = args
log.debug('cloning %s to %s', original_ua.path, target_ua_path)
ua = original_ua.clone(target_ua_path)
log.debug('done cloning to %s', original_ua.path)
return ua
def multisign(original_path, cred_dirs_to_output_paths, info_props=None):
""" Given a path to an app,
a mapping of credential directories to desired output paths,
optional info.plist properties to overwrite,
produce re-signed versions of the app as desired.
See: multisign_archive, which this wraps.
Returns an array of tuples of [(credentials_dir, resigned app path)...]
"""
archive = archive_factory(original_path)
if archive is None:
log.debug("%s didn't look like an app...", original_path)
return None
return multisign_archive(archive, cred_dirs_to_output_paths, info_props)
def multisign_archive(archive, cred_dirs_to_output_paths, info_props=None):
""" Given an isign.archive object,
a mapping of credential directories to desired output paths,
optional info.plist properties to overwrite,
produce re-signed versions of the IPA.
If info_props are provided, it will overwrite those properties in
the app's Info.plist.
Returns an array of tuples of [(credentials_dir, resigned app path)...]
"""
# get ready for multiple processes...
p = multiprocessing.Pool(MAX_PROCESSES)
# ua is potentially an isign.archive.UncompressedArchive
ua = None
results = []
try:
ua = archive.unarchive_to_temp()
if info_props:
# Override info.plist props
ua.bundle.update_info_props(info_props)
# Since the signing process rewrites files, we must first create uncompressed archives
# for each credentials_directory.
# The first is simply the uncompressed archive we just made
uas = [ua]
# But the rest need to be copied. This might take a while, so let's do it in parallel
# this will copy them to /path/to/uncompressedArchive_1, .._2, and so on
# and make UncompressedArchive objects that can be used for resigning
target_ua_paths = []
for i in range(1, len(cred_dirs_to_output_paths)):
target_ua_paths.append((ua, ua.path + '_' + str(i)))
uas += p.map(clone_ua, target_ua_paths)
# now we should have one UncompressedArchive for every credential directory
assert len(uas) == len(cred_dirs_to_output_paths)
# We will now construct arguments for all the resignings
resign_args = []
for i, (cred_dir, output_path) in enumerate(cred_dirs_to_output_paths.items()):
resign_args.append((uas[i], cred_dir, output_path))
log.debug('resign args: %s', resign_args)
# In parallel, resign each uncompressed archive with supplied credentials,
# and make archives in the desired paths.
results = p.map(resign, resign_args)
except isign.NotSignable as e:
msg = "Not signable: <{0}>: {1}\n".format(archive.path, e)
log.error(msg)
raise
finally:
if ua is not None and isdir(ua.path):
ua.remove()
return results
================================================
FILE: isign/signable.py
================================================
# -*- coding: utf-8 -*-
#
# Represents a file that can be signed. A file that
# conforms to the Mach-O ABI.
#
# Executable, dylib, or framework.
#
from abc import ABCMeta
from codesig import (Codesig,
EntitlementsSlot,
ResourceDirSlot,
RequirementsSlot,
ApplicationSlot,
DerEntitlementsSlot,
InfoSlot)
import logging
import macho
from makesig import make_signature
import os
import tempfile
import utils
import shutil
log = logging.getLogger(__name__)
class Signable(object):
__metaclass__ = ABCMeta
slot_classes = []
def __init__(self, bundle, path, signer):
log.debug("working on {0}".format(path))
self.bundle = bundle
self.path = path
self.signer = signer
self.f = open(self.path, "rb")
self.f.seek(0, os.SEEK_END)
self.file_end = self.f.tell()
self.f.seek(0)
self.m = macho.MachoFile.parse_stream(self.f)
self.sign_from_scratch = False
# may set sign_from_scratch to True
self.arches = self._parse_arches()
def _parse_arches(self):
""" parse architectures and associated Codesig """
arch_macho = self.m.data
arches = []
if 'FatArch' in arch_macho:
log.debug('found fat binary')
for i, arch in enumerate(arch_macho.FatArch):
log.debug('found fat slice: cputype {}, cpusubtype {}'.format(arch.cputype, arch.cpusubtype))
this_arch_macho = arch.MachO
log.debug('slice {}: arch offset: {}, size: {}'.format(i, arch.offset, arch.size))
arch_object = self._get_arch(this_arch_macho,
arch.offset,
arch.size)
arch_object['fat_index'] = i
arches.append(arch_object)
else:
log.debug('found thin binary: cputype {}, cpusubtype {}'.format(arch_macho.cputype, arch_macho.cpusubtype))
arches.append(self._get_arch(arch_macho,
0,
self.file_end))
return arches
def _get_arch(self, macho, arch_offset, arch_size):
arch = {'macho': macho, 'arch_offset': arch_offset, 'arch_size': arch_size}
arch['cmds'] = {}
for cmd in macho.commands:
name = cmd.cmd
arch['cmds'][name] = cmd
codesig_data = None
if 'LC_CODE_SIGNATURE' in arch['cmds']:
arch['lc_codesig'] = arch['cmds']['LC_CODE_SIGNATURE']
codesig_offset = arch['macho'].macho_start + arch['lc_codesig'].data.dataoff
self.f.seek(codesig_offset)
codesig_data = self.f.read(arch['lc_codesig'].data.datasize)
# log.debug("codesig len: {0}".format(len(codesig_data)))
else:
log.info("signing from scratch!")
self.sign_from_scratch = True
entitlements_file = self.bundle.get_entitlements_path() # '/path/to/some/entitlements.plist'
# Stage 1: Fake signature
fake_codesig_data = make_signature(macho, arch_offset, arch_size, arch['cmds'], self.f, entitlements_file,
0, self.signer, self.bundle.get_info_prop('CFBundleIdentifier'))
# We're stripping out the fake LC_CODE_SIGNATURE command, which we know has a size of 16, so we need to
# decrement the overall sizeofcmds
macho.ncmds -= 1
macho.commands = macho.commands[:-1]
macho.sizeofcmds -= 16
# Get the length
fake_codesig = Codesig(self, fake_codesig_data)
fake_codesig.set_signature(self.signer)
fake_codesig.update_offsets()
fake_codesig_length = len(fake_codesig.build_data())
log.debug("fake codesig length: {}".format(fake_codesig_length))
# stage 2: real signature
codesig_data = make_signature(macho, arch_offset, arch_size, arch['cmds'], self.f, entitlements_file,
fake_codesig_length, self.signer,
self.bundle.get_info_prop('CFBundleIdentifier'))
arch['lc_codesig'] = arch['cmds']['LC_CODE_SIGNATURE']
arch['codesig'] = Codesig(self, codesig_data)
arch['codesig_len'] = len(codesig_data)
if self.sign_from_scratch:
arch['codesig_data'] = codesig_data
return arch
def _sign_arch(self, arch, app, signer):
# Returns slice-relative offset, code signature blob
arch['codesig'].resign(app, signer)
new_codesig_data = arch['codesig'].build_data()
new_codesig_len = len(new_codesig_data)
log.debug("new codesig len is: {0}".format(new_codesig_len))
padding_length = arch['codesig_len'] - new_codesig_len
new_codesig_data += "\x00" * padding_length
# log.debug("padded len: {0}".format(len(new_codesig_data)))
# log.debug("----")
cmd = arch['lc_codesig']
cmd.data.datasize = len(new_codesig_data)
cmd.bytes = macho.CodeSigRef.build(arch['lc_codesig'].data)
offset = cmd.data.dataoff
return offset, new_codesig_data
def should_fill_slot(self, codesig, slot):
slot_class = slot.__class__
if slot_class not in self.slot_classes:
# This signable does not have this slot
return False
if self.sign_from_scratch:
return True
if slot_class == InfoSlot and not self.bundle.info_props_changed():
# No Info.plist changes, don't fill
return False
if slot_class == ApplicationSlot and not codesig.is_sha256_signature():
# Application slot only needs to be zeroed out when there's a sha256 layer
return False
return True
def get_changed_bundle_id(self):
# Return a bundle ID to assign if Info.plist's CFBundleIdentifier value was changed
if self.bundle.info_prop_changed('CFBundleIdentifier'):
return self.bundle.get_info_prop('CFBundleIdentifier')
else:
return None
def sign(self, app, signer):
temp = tempfile.NamedTemporaryFile('wb', delete=False)
# If signing fat binary from scratch, need special handling
# TODO: we assume that if any slice is unsigned, all slices are. This should be true in practice but
# we should still guard against this.
if self.sign_from_scratch and 'FatArch' in self.m.data:
# Fat binaries have more than 2 architectures, but thin ones only have one, so we assert that
assert len(self.arches) >= 1
# todo(markwang): Update fat headers and mach_start for each slice if needewd
log.debug('signing fat binary from scratch')
sorted_archs = sorted(self.arches, key=lambda arch: arch['arch_offset'])
prev_arch_end = 0
for arch in sorted_archs:
fatentry = arch['macho'] # has pointert to container
codesig_arch_offset, new_codesig_data = self._sign_arch(arch, app, signer)
codesig_file_offset = arch['arch_offset'] + codesig_arch_offset
log.debug('existing arch slice: cputype {}, cpusubtype {}, offset {}, size {}'
.format(fatentry.cputype, fatentry.cpusubtype, arch['arch_offset'], arch['arch_size']))
log.debug("codesig arch offset: {2}, file offset: {0}, len: {1}"
.format(codesig_file_offset, len(new_codesig_data), codesig_arch_offset))
assert codesig_file_offset >= (arch['arch_offset'] + arch['arch_size'])
# Store the old slice offset/sizes because we need them when we copy the data slices from self.f to temp
arch['old_arch_offset'] = arch['arch_offset']
arch['old_arch_size'] = arch['arch_size']
arch['codesig_arch_offset'] = codesig_arch_offset
arch['codesig_data'] = new_codesig_data
new_arch_size = codesig_arch_offset + len(new_codesig_data)
if prev_arch_end > arch['arch_offset']:
arch['arch_offset'] = utils.round_up(prev_arch_end, 16384)
prev_arch_end = arch['arch_offset'] + new_arch_size
arch['arch_size'] = new_arch_size
log.debug('new arch slice after codesig: offset {}, size {}'.format(arch['arch_offset'],
arch['arch_size']))
# write slices and code signatures in reverse order
for arch in reversed(sorted_archs):
self.f.seek(arch['old_arch_offset'])
temp.seek(arch['arch_offset'])
temp.write(self.f.read(arch['old_arch_size']))
temp.seek(arch['arch_offset'] + arch['codesig_arch_offset'])
temp.write(arch['codesig_data'])
fatarch_info = self.m.data.FatArch[arch['fat_index']]
fatarch_info.size = arch['arch_size']
fatarch_info.offset = arch['arch_offset']
else:
# copy self.f into temp, reset to beginning of file
self.f.seek(0)
temp.write(self.f.read())
temp.seek(0)
# write new codesign blocks for each arch
offset_fmt = ("offset: {2}, write offset: {0}, "
"new_codesig_data len: {1}")
for arch in self.arches:
offset, new_codesig_data = self._sign_arch(arch, app, signer)
write_offset = arch['macho'].macho_start + offset
log.debug(offset_fmt.format(write_offset,
len(new_codesig_data),
offset))
temp.seek(write_offset)
temp.write(new_codesig_data)
# write new headers
temp.seek(0)
macho.MachoFile.build_stream(self.m, temp)
temp.close()
# make copy have same permissions
mode = os.stat(self.path).st_mode
os.chmod(temp.name, mode)
# log.debug("moving temporary file to {0}".format(self.path))
shutil.move(temp.name, self.path)
class Executable(Signable):
""" The main executable of an app. """
slot_classes = [EntitlementsSlot,
DerEntitlementsSlot,
ResourceDirSlot,
RequirementsSlot,
ApplicationSlot,
InfoSlot]
class Dylib(Signable):
""" A dynamic library that isn't part of its own bundle, e.g.
the Swift libraries.
TODO: Dylibs have an info slot, however the Info.plist is embedded in the __TEXT section
of the file (__info_plist) instead of being a seperate file.
Add read/write of the embedded Info.plist so we can include InfoSlot below.
"""
slot_classes = [EntitlementsSlot,
DerEntitlementsSlot,
RequirementsSlot]
class Appex(Signable):
""" An app extension """
slot_classes = [EntitlementsSlot,
DerEntitlementsSlot,
RequirementsSlot,
InfoSlot]
class Framework(Signable):
""" The main executable of a Framework, which is a library of sorts
but is bundled with both files and code """
slot_classes = [ResourceDirSlot,
RequirementsSlot,
InfoSlot]
================================================
FILE: isign/signer.py
================================================
# -*- coding: utf-8 -*-
#
# Small object that can be passed around easily, that represents
# our signing credentials, and can sign data.
#
# Unfortunately the installed python OpenSSL library doesn't
# offer what we need for cms, so we also need to shell out to the openssl
# tool, and make sure it's the right version.
from distutils import spawn
from exceptions import (ImproperCredentials,
MissingCredentials,
OpenSslFailure)
import logging
from OpenSSL import crypto
import os
import os.path
import subprocess
import re
OPENSSL = os.getenv('OPENSSL', spawn.find_executable('openssl'))
# modern OpenSSL versions look like '0.9.8zd'. Use a regex to parse
OPENSSL_VERSION_RE = re.compile(r'(\d+).(\d+).(\d+)(\w*)')
MINIMUM_OPENSSL_VERSION = '1.0.1'
log = logging.getLogger(__name__)
def openssl_command(args, data=None, expect_err=False):
""" Given array of args, and optionally data to write,
return results of openssl command.
Some commands always write something to stderr, so allow
for that with the expect_err param. """
cmd = [OPENSSL] + args
cmd_str = ' '.join(cmd)
# log.debug('running command ' + cmd_str)
proc = subprocess.Popen(cmd,
stdin=subprocess.PIPE,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
if data is not None:
proc.stdin.write(data)
out, err = proc.communicate()
if not expect_err:
if err is not None and err != '':
log.error("Command `{0}` returned error:\n{1}".format(cmd_str, err))
if proc.returncode != 0:
msg = "openssl command `{0}` failed, see log for error".format(cmd_str)
raise OpenSslFailure(msg)
if expect_err:
return (out, err)
else:
return out
def get_installed_openssl_version():
version_line = openssl_command(['version'])
# e.g. 'OpenSSL 0.9.8zd 8 Jan 2015'
return re.split(r'\s+', version_line)[1]
def is_openssl_version_ok(version, minimum):
""" check that the openssl tool is at least a certain version """
version_tuple = openssl_version_to_tuple(version)
minimum_tuple = openssl_version_to_tuple(minimum)
return version_tuple >= minimum_tuple
def openssl_version_to_tuple(s):
""" OpenSSL uses its own versioning scheme, so we convert to tuple,
for easier comparison """
search = re.search(OPENSSL_VERSION_RE, s)
if search is not None:
return search.groups()
return ()
class Signer(object):
""" collaborator, holds the keys, identifiers for signer,
and knows how to sign data """
def __init__(self,
signer_key_file=None,
signer_cert_file=None,
apple_cert_file=None,
team_id=None):
""" signer_key_file = your org's key .pem
signer_cert_file = your org's cert .pem
apple_cert_file = apple certs in .pem form
team_id = your Apple Organizational Unit code """
for filename in [signer_key_file, signer_cert_file, apple_cert_file]:
if not os.path.exists(filename):
msg = "Can't find {0}".format(filename)
log.warn(msg)
raise MissingCredentials(msg)
self.signer_key_file = signer_key_file
self.signer_cert_file = signer_cert_file
self.apple_cert_file = apple_cert_file
team_id = self._get_team_id()
if team_id is None:
raise ImproperCredentials("Cert file does not contain Subject line"
"with Apple Organizational Unit (OU)")
self.team_id = team_id
self.check_openssl_version()
def check_openssl_version(self):
openssl_version = get_installed_openssl_version()
if not is_openssl_version_ok(openssl_version, MINIMUM_OPENSSL_VERSION):
msg = "Signing may not work: OpenSSL version is {0}, need {1} !"
log.warn(msg.format(openssl_version, MINIMUM_OPENSSL_VERSION))
def sign(self, data, digest_algorithm = "sha1"):
""" sign data, return filehandle """
cmd = [
"cms",
"-sign", "-binary", "-nosmimecap",
"-certfile", self.apple_cert_file,
"-signer", self.signer_cert_file,
"-inkey", self.signer_key_file,
"-keyform", "pem",
"-outform", "DER",
"-md", digest_algorithm
]
signature = openssl_command(cmd, data)
log.debug("in length: {}, out length: {}".format(len(data), len(signature)))
# in some cases we've seen this return a zero length file.
# Misconfigured machines?
if len(signature) < 128:
too_small_msg = "Command `{0}` returned success, but signature "
"seems too small ({1} bytes)"
raise OpenSslFailure(too_small_msg.format(' '.join(cmd),
len(signature)))
return signature
def get_common_name(self):
""" read in our cert, and get our Common Name """
with open(self.signer_cert_file, 'rb') as fh:
cert = crypto.load_certificate(crypto.FILETYPE_PEM, fh.read())
subject = cert.get_subject()
return dict(subject.get_components())['CN']
def _log_parsed_asn1(self, data):
cmd = ['asn1parse', '-inform', 'DER' '-i']
parsed_asn1 = openssl_command(cmd)
log.debug(parsed_asn1)
def _get_team_id(self):
""" Same as Apple Organizational Unit. Should be in the cert """
team_id = None
cmd = [
'x509',
'-in', self.signer_cert_file,
'-text',
'-noout'
]
certificate_info = openssl_command(cmd)
subject_with_ou_match = re.compile(r'\s+Subject:.*OU\s?=\s?(\w+)')
for line in certificate_info.splitlines():
match = subject_with_ou_match.match(line)
if match is not None:
team_id = match.group(1)
break
return team_id
================================================
FILE: isign/utils.py
================================================
# -*- coding: utf-8 -*-
import binascii
import re
def print_data(data):
hexstring = binascii.hexlify(data)
n = 80
split_string = "\n".join([hexstring[i:i+n] for i in range(0, len(hexstring), n)])
print split_string
def round_up(x, k):
return ((x + k - 1) & -k)
def print_structure(container, struct):
actual_data = struct.build(container)
return "{}".format(struct.parse(actual_data))
# remove control character in string
# This will affect the executable_name part in path
def remove_control_char(str):
return re.compile('[\\x00-\\x08\\x0b-\\x0c\\x0e-\\x1f]').sub('', str)
================================================
FILE: jenkins.sh
================================================
#!/bin/bash
# figure out our package name from the name of repo
pushd $(dirname $0) >/dev/null
package_name=$(basename $PWD)
package=$(echo $package_name | sed 's/-/_/g')
popd >/dev/null
version=$(./version.sh)
# this communicates to setup.py to use the version
# number we just made, rather than generating another
export PYTHON_PACKAGE_VERSION=${version}
make_venv() {
# For some reason mkvirtualenv returns with exit code 1 on success. So we
# have to just continue.
virtualenv $TMPDIR || true
source $TMPDIR/bin/activate
pip install -r dev/requirements.txt
}
build_release() {
python setup.py sdist
release=${package_name}-${version}.tar.gz
}
test_release() {
rm -rf dist-release && mkdir -p dist-release
echo "pip install dist/$release"
pip install dist/$release
echo -e "\nInstalled packages:"
pip freeze -l
echo
mv $package ${package}.testing
./run_tests.sh
mv dist/$release dist-release/
}
# to push tags: add the repo to the "bots" team
tag_release() {
tag="v$version"
echo "Tagging $head as $tag"
git tag $tag $head
git push origin $tag
}
update_pypi() {
# always succeed with upload - transient errors with pypi
# should not cause red build
twine upload dist-release/$release || true
}
cleanup() {
rm -rf $TMPDIR
if [[ -d ${package}.testing ]]; then
mv ${package}.testing $package
fi
}
set -e
TMPDIR=$(mktemp -d /tmp/${package_name}.XXXXXXXX)
trap cleanup 0
make_venv
build_release
test_release
tag_release
update_pypi
cleanup
================================================
FILE: run_tests.sh
================================================
#!/bin/bash -e
name=$(basename $PWD)
package=$(echo $name | sed 's/-/_/g')
# look for required apps
for app in unzip zip; do
if ! which $app >/dev/null; then
echo "Missing application: $app"
exit 1
fi
done
# run test suite
find . -name '*.pyc' -delete
pushd tests >/dev/null
version=$(python -c "import $package; print ${package}.__version__")
echo "Testing $name v${version}"
nosetests
popd >/dev/null
================================================
FILE: setup.cfg
================================================
[metadata]
description-file = README.rst
================================================
FILE: setup.py
================================================
# encoding: utf-8
from setuptools import setup, find_packages
from codecs import open # to use a consistent encoding
from subprocess import check_output
from os import path, environ
here = path.abspath(path.dirname(__file__))
if path.exists(path.join(here, "version.sh")): # development
if 'PYTHON_PACKAGE_VERSION' in environ:
version = environ['PYTHON_PACKAGE_VERSION']
else:
version = check_output(path.join(here, "version.sh")).strip()
package_name = path.basename(here)
else: # source package
with open(path.join(here, "PKG-INFO")) as f:
for line in f.readlines():
if line.startswith("Version:"):
version = line.split(":")[1].strip()
elif line.startswith("Name:"):
package_name = line.split(":")[1].strip()
package = package_name.replace('-', '_')
setup(
name=package_name,
version=version,
description='Re-signing iOS apps without Apple tools',
url='https://github.com/saucelabs/{}'.format(package_name),
download_url='https://github.com/saucelabs/{}/tarball/v{}'.format(package_name, version),
author='Sauce Labs',
author_email='dev@saucelabs.com',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'Topic :: Utilities',
'License :: OSI Approved :: Apache Software License',
'Programming Language :: Python :: 2.7',
],
keywords=['ios', 'app', 'signature', 'codesign', 'sign', 'resign'],
packages=find_packages(),
install_requires=[
'biplist==1.0.3',
'construct==2.5.2',
'memoizer==0.0.1',
'pyOpenSSL==18.0.0',
'pyasn1==0.4.8',
'six>=1.10.0'
],
package_data={
package: ['apple_credentials/applecerts.pem',
'code_resources_template.xml',
'version.json'],
},
scripts=['bin/isign',
'bin/multisign',
'bin/isign_export_creds.sh',
'bin/isign_guess_mobileprovision.sh']
)
================================================
FILE: tests/NotAnApp.txt
================================================
This file isn't actually an app.
================================================
FILE: tests/NotAnAppDir/README.md
================================================
This is a directory which isign shouldn't accept as being an AppArchive
================================================
FILE: tests/README.md
================================================
To test isign, we need to actually sign some apps. This directory contains
the tests and the test data AND the source files to generate that test data.
This directory includes a lot of stuff. It could probably be better organized.
Test data: to check that the library can detect non-apps:
* NotAnApp.ipa
* NotAnApp.txt
Test data - compiled apps:
* Test.app
* Test.app.zip
* Test.ipa
* TestSimulator.app.zip
* TestWithFrameworks.ipa
Expected data from apps:
* Test.app.codesig.construct.txt
A short program to generate that expected data:
* generate_codesig_construct_txt.py
A test file, to see what happens when "helper" apps go wrong:
* bad_openssl
Self-signed/fake credentials, to make these tests even work
* credentials
Source to build the test apps:
* isignTestApp
* isignTestAppWithFrameworks
Helper for tests:
* `monitor_temp_file.py`
* `helpers.py`
Actual test code:
* `isign_base_test.py`
* `test_*.py:`
================================================
FILE: tests/Test.app/PkgInfo
================================================
APPL????
================================================
FILE: tests/Test.app/_CodeSignature/CodeResources
================================================
<?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>
<key>files</key>
<dict>
<key>Assets.car</key>
<data>
JTRsnG3T7BKKYvgn8rcVKnVf67c=
</data>
<key>Base.lproj/LaunchScreen.storyboardc/01J-lp-oVM-view-Ze5-6b-2t3.nib</key>
<dict>
<key>hash</key>
<data>
eMYxMmbROeqxPMenCgfCsYRPQKs=
</data>
<key>optional</key>
<true/>
</dict>
<key>Base.lproj/LaunchScreen.storyboardc/Info.plist</key>
<dict>
<key>hash</key>
<data>
n2t8gsDpfE6XkhG31p7IQJRxTxU=
</data>
<key>optional</key>
<true/>
</dict>
<key>Base.lproj/LaunchScreen.storyboardc/UIViewController-01J-lp-oVM.nib</key>
<dict>
<key>hash</key>
<data>
esMacQh55MAAFCgvM1T6IE5te+s=
</data>
<key>optional</key>
<true/>
</dict>
<key>Base.lproj/Main.storyboardc/8rJ-Kc-sve-view-QS5-Rx-YEW.nib</key>
<dict>
<key>hash</key>
<data>
eIL8TQj/c89wIYSRBONbQO58A0k=
</data>
<key>optional</key>
<true/>
</dict>
<key>Base.lproj/Main.storyboardc/9pv-A4-QxB-view-tsR-hK-woN.nib</key>
<dict>
<key>hash</key>
<data>
uMIVkX919bfq77m9Y4eVapG/uAI=
</data>
<key>optional</key>
<true/>
</dict>
<key>Base.lproj/Main.storyboardc/Info.plist</key>
<dict>
<key>hash</key>
<data>
/BgmwrRK8ElRpzfemriLZvm3U/Y=
</data>
<key>optional</key>
<true/>
</dict>
<key>Base.lproj/Main.storyboardc/UITabBarController-49e-Tb-3d3.nib</key>
<dict>
<key>hash</key>
<data>
3Z4RvYT6WblSIhmHjkV4OCT0478=
</data>
<key>optional</key>
<true/>
</dict>
<key>Frameworks/libswiftContacts.dylib</key>
<data>
XAUF5AOf/FKJ0FOaBryoXydYPkg=
</data>
<key>Frameworks/libswiftCore.dylib</key>
<data>
oPSCk2x86K18iCXpKgzk+xPXrUM=
</data>
<key>Frameworks/libswiftCoreGraphics.dylib</key>
<data>
lFIRixuLPuPvyFEA/4UmWEoWsT4=
</data>
<key>Frameworks/libswiftCoreImage.dylib</key>
<data>
8w2X9ce86i4QEh2pxcwWJOw/hmY=
</data>
<key>Frameworks/libswiftDarwin.dylib</key>
<data>
dkfj0DypEBEArQE2yWzHKZXxeHM=
</data>
<key>Frameworks/libswiftDispatch.dylib</key>
<data>
jyXJbh2S33qT0cSQuNl8KYPMzBE=
</data>
<key>Frameworks/libswiftFoundation.dylib</key>
<data>
k5QXs7u4hyTXE+5NlHlz9KmMigg=
</data>
<key>Frameworks/libswiftObjectiveC.dylib</key>
<data>
Z1hjxXaY8/0BWfiaYAtBLah9NdQ=
</data>
<key>Frameworks/libswiftUIKit.dylib</key>
<data>
+mEoScPMQf+x9JhE1vClNhvkZIc=
</data>
<key>Info.plist</key>
<data>
yBg+lr48gYGDv7irgz8Co91E2c8=
</data>
<key>PkgInfo</key>
<data>
n57qDP4tZfLD1rCS43W0B4LQjzE=
</data>
<key>embedded.mobileprovision</key>
<data>
VHqTakGK9srUKBbgpbbbYG6u4bk=
</data>
</dict>
<key>files2</key>
<dict>
<key>Assets.car</key>
<data>
JTRsnG3T7BKKYvgn8rcVKnVf67c=
</data>
<key>Base.lproj/LaunchScreen.storyboardc/01J-lp-oVM-view-Ze5-6b-2t3.nib</key>
<dict>
<key>hash</key>
<data>
eMYxMmbROeqxPMenCgfCsYRPQKs=
</data>
<key>optional</key>
<
gitextract_wnigoa8g/ ├── .gitignore ├── CONDUCT.md ├── INSTALL.sh ├── LICENSE.txt ├── MANIFEST.in ├── PKG-INFO ├── PREREQUISITES.rst ├── README.rst ├── apple/ │ ├── README.rst │ ├── provisions.py │ └── provisions.sh ├── bin/ │ ├── isign │ ├── isign_export_creds.sh │ ├── isign_guess_mobileprovision.sh │ ├── make_seal │ ├── multisign │ └── pprint_codesig ├── dev/ │ ├── requirements.txt │ └── setup.sh ├── docs/ │ ├── applecerts.rst │ ├── codedirectory.rst │ ├── credentials.rst │ └── speed.rst ├── isign/ │ ├── __init__.py │ ├── apple_credentials/ │ │ └── README.rst │ ├── archive.py │ ├── bundle.py │ ├── code_resources.py │ ├── code_resources_template.xml │ ├── codesig.py │ ├── der_encoder.py │ ├── exceptions.py │ ├── isign.py │ ├── macho.py │ ├── macho_cs.py │ ├── makesig.py │ ├── multisign.py │ ├── signable.py │ ├── signer.py │ └── utils.py ├── jenkins.sh ├── run_tests.sh ├── setup.cfg ├── setup.py ├── tests/ │ ├── NotAnApp.ipa │ ├── NotAnApp.txt │ ├── NotAnAppDir/ │ │ └── README.md │ ├── README.md │ ├── Test.app/ │ │ ├── Assets.car │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboardc/ │ │ │ │ ├── 01J-lp-oVM-view-Ze5-6b-2t3.nib │ │ │ │ ├── Info.plist │ │ │ │ └── UIViewController-01J-lp-oVM.nib │ │ │ └── Main.storyboardc/ │ │ │ ├── 8rJ-Kc-sve-view-QS5-Rx-YEW.nib │ │ │ ├── 9pv-A4-QxB-view-tsR-hK-woN.nib │ │ │ ├── Info.plist │ │ │ └── UITabBarController-49e-Tb-3d3.nib │ │ ├── Info.plist │ │ ├── PkgInfo │ │ ├── _CodeSignature/ │ │ │ └── CodeResources │ │ ├── build.sh │ │ ├── isignTestApp │ │ ├── isignTestApp.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace/ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcuserdata/ │ │ │ │ └── neilk.xcuserdatad/ │ │ │ │ └── UserInterfaceState.xcuserstate │ │ │ └── xcuserdata/ │ │ │ └── neilk.xcuserdatad/ │ │ │ └── xcschemes/ │ │ │ ├── isignTestApp.xcscheme │ │ │ └── xcschememanagement.plist │ │ ├── isignTestAppTests/ │ │ │ ├── Info.plist │ │ │ └── isignTestAppTests.swift │ │ └── isignTestAppUITests/ │ │ ├── Info.plist │ │ └── isignTestAppUITests.swift │ ├── Test.app.codesig.construct.txt │ ├── Test.ipa │ ├── TestWithFrameworks.ipa │ ├── Test_unsigned_fat.app/ │ │ ├── Assets.car │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboardc/ │ │ │ │ ├── 01J-lp-oVM-view-Ze5-6b-2t3.nib │ │ │ │ ├── Info.plist │ │ │ │ └── UIViewController-01J-lp-oVM.nib │ │ │ └── Main.storyboardc/ │ │ │ ├── 8rJ-Kc-sve-view-QS5-Rx-YEW.nib │ │ │ ├── 9pv-A4-QxB-view-tsR-hK-woN.nib │ │ │ ├── Info.plist │ │ │ └── UITabBarController-49e-Tb-3d3.nib │ │ ├── Info.plist │ │ ├── PkgInfo │ │ ├── build.sh │ │ ├── isignTestApp │ │ ├── isignTestApp.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace/ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcuserdata/ │ │ │ │ └── neilk.xcuserdatad/ │ │ │ │ └── UserInterfaceState.xcuserstate │ │ │ └── xcuserdata/ │ │ │ └── neilk.xcuserdatad/ │ │ │ └── xcschemes/ │ │ │ ├── isignTestApp.xcscheme │ │ │ └── xcschememanagement.plist │ │ ├── isignTestAppTests/ │ │ │ ├── Info.plist │ │ │ └── isignTestAppTests.swift │ │ └── isignTestAppUITests/ │ │ ├── Info.plist │ │ └── isignTestAppUITests.swift │ ├── Test_unsigned_thin.app/ │ │ ├── Assets.car │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboardc/ │ │ │ │ ├── 01J-lp-oVM-view-Ze5-6b-2t3.nib │ │ │ │ ├── Info.plist │ │ │ │ └── UIViewController-01J-lp-oVM.nib │ │ │ └── Main.storyboardc/ │ │ │ ├── 8rJ-Kc-sve-view-QS5-Rx-YEW.nib │ │ │ ├── 9pv-A4-QxB-view-tsR-hK-woN.nib │ │ │ ├── Info.plist │ │ │ └── UITabBarController-49e-Tb-3d3.nib │ │ ├── Info.plist │ │ ├── PkgInfo │ │ ├── build.sh │ │ ├── isignTestApp │ │ ├── isignTestApp.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace/ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcuserdata/ │ │ │ │ └── neilk.xcuserdatad/ │ │ │ │ └── UserInterfaceState.xcuserstate │ │ │ └── xcuserdata/ │ │ │ └── neilk.xcuserdatad/ │ │ │ └── xcschemes/ │ │ │ ├── isignTestApp.xcscheme │ │ │ └── xcschememanagement.plist │ │ ├── isignTestAppTests/ │ │ │ ├── Info.plist │ │ │ └── isignTestAppTests.swift │ │ └── isignTestAppUITests/ │ │ ├── Info.plist │ │ └── isignTestAppUITests.swift │ ├── bad_openssl │ ├── credentials/ │ │ ├── README.rst │ │ ├── makeFakePprof.sh │ │ ├── test.cert.pem │ │ ├── test.key.pem │ │ ├── test.mobileprovision │ │ └── test.mobileprovision.plist │ ├── credentials_std_names/ │ │ ├── README.rst │ │ ├── certificate.pem │ │ └── isign.mobileprovision │ ├── credentials_std_names_2/ │ │ ├── README.rst │ │ ├── certificate.pem │ │ ├── isign.mobileprovision │ │ └── key.pem │ ├── generate_codesig_construct_txt.py │ ├── isignTestApp/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.sh │ │ ├── exportOptions.plist │ │ ├── isignTestApp/ │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets/ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── first.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── second.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj/ │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ └── Main.storyboard │ │ │ ├── FirstViewController.swift │ │ │ ├── Info.plist │ │ │ └── SecondViewController.swift │ │ ├── isignTestApp.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace/ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcuserdata/ │ │ │ │ └── neilk.xcuserdatad/ │ │ │ │ └── UserInterfaceState.xcuserstate │ │ │ └── xcuserdata/ │ │ │ └── neilk.xcuserdatad/ │ │ │ └── xcschemes/ │ │ │ ├── isignTestApp.xcscheme │ │ │ └── xcschememanagement.plist │ │ ├── isignTestAppTests/ │ │ │ ├── Info.plist │ │ │ └── isignTestAppTests.swift │ │ └── isignTestAppUITests/ │ │ ├── Info.plist │ │ └── isignTestAppUITests.swift │ ├── isignTestAppWithFrameworks/ │ │ ├── .gitignore │ │ ├── Podfile │ │ ├── README.md │ │ ├── build.sh │ │ ├── exportOptions.plist │ │ ├── isignTestApp/ │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets/ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── first.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── second.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj/ │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ └── Main.storyboard │ │ │ ├── FirstViewController.swift │ │ │ ├── Info.plist │ │ │ └── SecondViewController.swift │ │ ├── isignTestApp.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace/ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcuserdata/ │ │ │ │ └── neilk.xcuserdatad/ │ │ │ │ └── UserInterfaceState.xcuserstate │ │ │ └── xcuserdata/ │ │ │ └── neilk.xcuserdatad/ │ │ │ └── xcschemes/ │ │ │ ├── isignTestApp.xcscheme │ │ │ └── xcschememanagement.plist │ │ ├── isignTestApp.xcworkspace/ │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcuserdata/ │ │ │ └── neilk.xcuserdatad/ │ │ │ └── UserInterfaceState.xcuserstate │ │ ├── isignTestAppTests/ │ │ │ ├── Info.plist │ │ │ └── isignTestAppTests.swift │ │ └── isignTestAppUITests/ │ │ ├── Info.plist │ │ └── isignTestAppUITests.swift │ ├── isign_base_test.py │ ├── monitor_temp_file.py │ ├── sample-entitlements.plist │ ├── test_archive.py │ ├── test_creds_dir.py │ ├── test_entitlements.py │ ├── test_helpers.py │ ├── test_multisign.py │ ├── test_parsing.py │ ├── test_public_interface.py │ ├── test_sig.py │ ├── test_versioning.py │ └── test_versus_apple.py └── version.sh
SYMBOL INDEX (265 symbols across 27 files)
FILE: apple/provisions.py
class ReceivedApp (line 20) | class ReceivedApp(object):
method __init__ (line 21) | def __init__(self, path):
method unpack_to_dir (line 24) | def unpack_to_dir(self, unpack_dir):
class ReceivedIpaApp (line 31) | class ReceivedIpaApp(ReceivedApp):
method unpack_to_dir (line 32) | def unpack_to_dir(self, target_dir):
class App (line 37) | class App(object):
method __init__ (line 38) | def __init__(self, path):
method get_app_dir (line 46) | def get_app_dir(self):
method provision (line 49) | def provision(self, provision_path):
method create_entitlements (line 53) | def create_entitlements(self):
method codesign (line 82) | def codesign(self, certificate, path, extra_args=[]):
method sign (line 85) | def sign(self, certificate):
method package (line 97) | def package(self, output_path):
class IpaApp (line 104) | class IpaApp(App):
method _get_payload_dir (line 105) | def _get_payload_dir(self):
method get_app_dir (line 108) | def get_app_dir(self):
method package (line 117) | def package(self, output_path):
function absolute_path_argument (line 132) | def absolute_path_argument(path):
function exists_absolute_path_argument (line 136) | def exists_absolute_path_argument(path):
function app_argument (line 142) | def app_argument(path):
function parse_args (line 155) | def parse_args():
FILE: isign/archive.py
function get_helper (line 31) | def get_helper(helper_name):
function make_temp_dir (line 42) | def make_temp_dir():
function get_watchkit_paths (line 46) | def get_watchkit_paths(root_bundle_path):
function process_watchkit (line 73) | def process_watchkit(root_bundle_path, should_remove=False):
class Archive (line 88) | class Archive(object):
method unarchive_to_temp (line 94) | def unarchive_to_temp(self):
method archive (line 99) | def archive(cls, path, output_path):
method get_info (line 104) | def get_info(cls, path):
method precheck (line 109) | def precheck(cls, path):
method find_bundle_dir (line 114) | def find_bundle_dir(cls, path):
class AppArchive (line 119) | class AppArchive(Archive):
method find_bundle_dir (line 124) | def find_bundle_dir(cls, path):
method _get_plist_path (line 130) | def _get_plist_path(cls, path):
method get_info (line 134) | def get_info(cls, path):
method precheck (line 138) | def precheck(cls, path):
method archive (line 149) | def archive(cls, path, output_path):
method __init__ (line 155) | def __init__(self, path):
method unarchive_to_temp (line 160) | def unarchive_to_temp(self):
class AppZipArchive (line 169) | class AppZipArchive(Archive):
method is_helpers_present (line 177) | def is_helpers_present(cls):
method is_archive_extension_match (line 188) | def is_archive_extension_match(cls, path):
method find_bundle_dir (line 198) | def find_bundle_dir(cls, zipfile_obj):
method _get_plist_path (line 216) | def _get_plist_path(cls, relative_bundle_dir):
method precheck (line 220) | def precheck(cls, path):
method get_info (line 246) | def get_info(cls, relative_bundle_dir, zipfile_obj):
method __init__ (line 251) | def __init__(self, path):
method unarchive_to_temp (line 258) | def unarchive_to_temp(self):
method archive (line 266) | def archive(cls, containing_dir, output_path):
class IpaArchive (line 290) | class IpaArchive(AppZipArchive):
class UncompressedArchive (line 297) | class UncompressedArchive(object):
method __init__ (line 305) | def __init__(self, path, relative_bundle_dir, archive_class):
method archive (line 317) | def archive(self, output_path):
method clone (line 322) | def clone(self, target_path):
method remove (line 330) | def remove(self):
function archive_factory (line 338) | def archive_factory(path):
function view (line 350) | def view(input_path):
function resign (line 367) | def resign(input_path,
FILE: isign/bundle.py
function is_info_plist_native (line 29) | def is_info_plist_native(plist):
class Bundle (line 37) | class Bundle(object):
method __init__ (line 44) | def __init__(self, path):
method get_entitlements_path (line 59) | def get_entitlements_path(self):
method get_executable_path (line 62) | def get_executable_path(self):
method update_info_props (line 77) | def update_info_props(self, new_props):
method info_props_changed (line 110) | def info_props_changed(self):
method info_prop_changed (line 113) | def info_prop_changed(self, key):
method get_info_prop (line 121) | def get_info_prop(self, key):
method sign_dylibs (line 124) | def sign_dylibs(self, signer, path):
method sign (line 130) | def sign(self, signer):
method resign (line 174) | def resign(self, signer):
class Framework (line 180) | class Framework(Bundle):
method __init__ (line 189) | def __init__(self, path):
class App (line 193) | class App(Bundle):
method __init__ (line 201) | def __init__(self, path):
method provision (line 208) | def provision(self, provision_path):
method extract_entitlements (line 212) | def extract_entitlements(provision_path):
method write_entitlements (line 233) | def write_entitlements(self, entitlements):
method resign (line 239) | def resign(self, signer, provisioning_profile, alternate_entitlements_...
FILE: isign/code_resources.py
function writeValue (line 26) | def writeValue(self, value):
class PathRule (line 41) | class PathRule(object):
method __init__ (line 48) | def __init__(self, pattern='', properties=None):
method is_optional (line 70) | def is_optional(self):
method is_omitted (line 73) | def is_omitted(self):
method is_nested (line 76) | def is_nested(self):
method is_exclusion (line 79) | def is_exclusion(self):
method is_top (line 82) | def is_top(self):
method matches (line 85) | def matches(self, path):
method __str__ (line 88) | def __str__(self):
class ResourceBuilder (line 92) | class ResourceBuilder(object):
method __init__ (line 95) | def __init__(self, app_path, rules_data, respect_omissions=False, incl...
method find_rule (line 104) | def find_rule(self, path):
method get_rule_and_paths (line 116) | def get_rule_and_paths(self, root, path):
method scan (line 122) | def scan(self):
function get_template (line 183) | def get_template():
function get_hash_hex (line 195) | def get_hash_hex(path, hash_type='sha1'):
function get_hash_binary (line 213) | def get_hash_binary(path, hash_type='sha1'):
function write_plist (line 218) | def write_plist(target_dir, plist):
function make_seal (line 229) | def make_seal(source_app_path, target_dir=None):
FILE: isign/codesig.py
class CodeDirectorySlot (line 15) | class CodeDirectorySlot(object):
method __init__ (line 19) | def __init__(self, codesig):
method get_hash (line 22) | def get_hash(self, hash_algorithm):
class DerEntitlementsSlot (line 30) | class DerEntitlementsSlot(CodeDirectorySlot):
method get_contents (line 33) | def get_contents(self):
class EntitlementsSlot (line 38) | class EntitlementsSlot(CodeDirectorySlot):
method get_contents (line 41) | def get_contents(self):
class ApplicationSlot (line 46) | class ApplicationSlot(CodeDirectorySlot):
method get_hash (line 49) | def get_hash(self, hash_algorithm):
class ResourceDirSlot (line 59) | class ResourceDirSlot(CodeDirectorySlot):
method __init__ (line 62) | def __init__(self, seal_path):
method get_contents (line 65) | def get_contents(self):
class RequirementsSlot (line 69) | class RequirementsSlot(CodeDirectorySlot):
method get_contents (line 72) | def get_contents(self):
class InfoSlot (line 77) | class InfoSlot(CodeDirectorySlot):
method __init__ (line 80) | def __init__(self, info_path):
method get_contents (line 83) | def get_contents(self):
class Codesig (line 89) | class Codesig(object):
method __init__ (line 91) | def __init__(self, signable, data):
method is_sha256_signature (line 96) | def is_sha256_signature(self):
method build_data (line 99) | def build_data(self):
method get_blobs (line 102) | def get_blobs(self, magic, min_expected=None, max_expected=None):
method get_blob_data (line 120) | def get_blob_data(self, blob):
method set_entitlements (line 124) | def set_entitlements(self, entitlements_path):
method set_requirements (line 144) | def set_requirements(self, signer):
method get_codedirectory_hash_index (line 198) | def get_codedirectory_hash_index(self, slot, code_directory):
method has_codedirectory_slot (line 204) | def has_codedirectory_slot(self, slot, code_directory):
method fill_codedirectory_slot (line 212) | def fill_codedirectory_slot(self, slot, code_directory, hash_algorithm):
method set_codedirectories (line 217) | def set_codedirectories(self, seal_path, info_path, signer):
method set_signature (line 262) | def set_signature(self, signer):
method update_offsets (line 287) | def update_offsets(self):
method resign (line 299) | def resign(self, bundle, signer):
FILE: isign/der_encoder.py
function encode (line 8) | def encode(element):
function _turn_into_der_structure (line 12) | def _turn_into_der_structure(element):
FILE: isign/exceptions.py
class NotSignable (line 5) | class NotSignable(Exception):
class NotMatched (line 11) | class NotMatched(Exception):
class MissingHelpers (line 17) | class MissingHelpers(NotSignable):
class MissingCredentials (line 22) | class MissingCredentials(Exception):
class ImproperCredentials (line 27) | class ImproperCredentials(Exception):
class OpenSslFailure (line 32) | class OpenSslFailure(Exception):
FILE: isign/isign.py
class NotSignable (line 18) | class NotSignable(Exception):
function get_credential_paths (line 23) | def get_credential_paths(directory, file_names=DEFAULT_CREDENTIAL_FILE_N...
function resign_with_creds_dir (line 53) | def resign_with_creds_dir(input_path,
function resign (line 61) | def resign(input_path,
function view (line 86) | def view(input_path):
FILE: isign/macho_cs.py
class PlistAdapter (line 14) | class PlistAdapter(Adapter):
method _encode (line 15) | def _encode(self, obj, context):
method _decode (line 18) | def _decode(self, obj, context):
FILE: isign/makesig.py
function make_arg (line 25) | def make_arg(data_type, arg):
function make_expr (line 51) | def make_expr(op, *args):
function make_requirements (line 68) | def make_requirements(drs, ident, common_name):
function build_code_directory_blob (line 110) | def build_code_directory_blob(hash_algorithm, teamID, ident_for_signatur...
function make_basic_codesig (line 177) | def make_basic_codesig(entitlements_file, drs, code_limit, hashes_sha1, ...
function make_signature (line 311) | def make_signature(arch_macho, arch_offset, arch_size, cmds, f, entitlem...
FILE: isign/multisign.py
function resign (line 14) | def resign(args):
function clone_ua (line 42) | def clone_ua(args):
function multisign (line 50) | def multisign(original_path, cred_dirs_to_output_paths, info_props=None):
function multisign_archive (line 70) | def multisign_archive(archive, cred_dirs_to_output_paths, info_props=None):
FILE: isign/signable.py
class Signable (line 28) | class Signable(object):
method __init__ (line 33) | def __init__(self, bundle, path, signer):
method _parse_arches (line 50) | def _parse_arches(self):
method _get_arch (line 73) | def _get_arch(self, macho, arch_offset, arch_size):
method _sign_arch (line 127) | def _sign_arch(self, arch, app, signer):
method should_fill_slot (line 147) | def should_fill_slot(self, codesig, slot):
method get_changed_bundle_id (line 167) | def get_changed_bundle_id(self):
method sign (line 174) | def sign(self, app, signer):
class Executable (line 264) | class Executable(Signable):
class Dylib (line 274) | class Dylib(Signable):
class Appex (line 287) | class Appex(Signable):
class Framework (line 295) | class Framework(Signable):
FILE: isign/signer.py
function openssl_command (line 29) | def openssl_command(args, data=None, expect_err=False):
function get_installed_openssl_version (line 59) | def get_installed_openssl_version():
function is_openssl_version_ok (line 65) | def is_openssl_version_ok(version, minimum):
function openssl_version_to_tuple (line 72) | def openssl_version_to_tuple(s):
class Signer (line 81) | class Signer(object):
method __init__ (line 84) | def __init__(self,
method check_openssl_version (line 108) | def check_openssl_version(self):
method sign (line 114) | def sign(self, data, digest_algorithm = "sha1"):
method get_common_name (line 137) | def get_common_name(self):
method _log_parsed_asn1 (line 144) | def _log_parsed_asn1(self, data):
method _get_team_id (line 149) | def _get_team_id(self):
FILE: isign/utils.py
function print_data (line 6) | def print_data(data):
function round_up (line 13) | def round_up(x, k):
function print_structure (line 17) | def print_structure(container, struct):
function remove_control_char (line 22) | def remove_control_char(str):
FILE: tests/generate_codesig_construct_txt.py
function log_to_stderr (line 13) | def log_to_stderr(level=logging.INFO):
FILE: tests/isign_base_test.py
class IsignBaseTest (line 13) | class IsignBaseTest(unittest.TestCase):
method setUp (line 39) | def setUp(self):
method tearDown (line 43) | def tearDown(self):
method resign (line 52) | def resign(self, filename, **args):
method unlink (line 61) | def unlink(self, path):
method get_temp_file (line 68) | def get_temp_file(self, prefix='isign-test-'):
method get_temp_dir (line 75) | def get_temp_dir(self, prefix='isign-test-'):
FILE: tests/monitor_temp_file.py
class MonitorTempFile (line 14) | class MonitorTempFile(object):
method mkdtemp (line 18) | def mkdtemp(cls, *args, **kwargs):
method mkstemp (line 24) | def mkstemp(cls, *args, **kwargs):
method NamedTemporaryFile (line 30) | def NamedTemporaryFile(cls, *args, **kwargs):
method start (line 36) | def start(cls):
method stop (line 57) | def stop(cls):
method get_temp_files (line 70) | def get_temp_files(cls):
method has_no_temp_files (line 74) | def has_no_temp_files(cls):
FILE: tests/test_archive.py
class TestArchive (line 8) | class TestArchive(IsignBaseTest):
method _test_good (line 10) | def _test_good(self, filename, klass):
method test_archive_factory_app (line 16) | def test_archive_factory_app(self):
method test_archive_factory_appzip (line 19) | def test_archive_factory_appzip(self):
method test_archive_factory_ipa (line 22) | def test_archive_factory_ipa(self):
method test_archive_factory_nonapp_dir (line 25) | def test_archive_factory_nonapp_dir(self):
method test_archive_factory_nonapp_ipa (line 29) | def test_archive_factory_nonapp_ipa(self):
method test_archive_factory_nonapp_txt (line 33) | def test_archive_factory_nonapp_txt(self):
method test_archive_factory_nonapp_simulator_app (line 37) | def test_archive_factory_nonapp_simulator_app(self):
class TestBundleInfo (line 42) | class TestBundleInfo(IsignBaseTest):
method _test_bundle_info (line 44) | def _test_bundle_info(self, filename):
method test_app_archive_info (line 50) | def test_app_archive_info(self):
method test_appzip_archive_info (line 53) | def test_appzip_archive_info(self):
method test_ipa_archive_info (line 56) | def test_ipa_archive_info(self):
class TestArchivePrecheck (line 60) | class TestArchivePrecheck(IsignBaseTest):
method test_precheck_app (line 62) | def test_precheck_app(self):
method test_precheck_appzip (line 65) | def test_precheck_appzip(self):
method test_precheck_ipa (line 68) | def test_precheck_ipa(self):
method test_bad_precheck_app (line 71) | def test_bad_precheck_app(self):
method test_bad_precheck_appzip (line 76) | def test_bad_precheck_appzip(self):
method test_bad_precheck_ipa (line 80) | def test_bad_precheck_ipa(self):
FILE: tests/test_creds_dir.py
class TestCredentialsDir (line 11) | class TestCredentialsDir(IsignBaseTest):
method test_creds_dir (line 13) | def test_creds_dir(self):
method test_bad_creds_dir (line 24) | def test_bad_creds_dir(self):
FILE: tests/test_entitlements.py
class TestEntitlements (line 8) | class TestEntitlements(IsignBaseTest):
method test_entitlements_extraction (line 9) | def test_entitlements_extraction(self):
FILE: tests/test_helpers.py
class MissingHelpersArchive (line 12) | class MissingHelpersArchive(AppZipArchive):
function dummy_find_executable (line 17) | def dummy_find_executable(name):
class TestHelpers (line 21) | class TestHelpers(IsignBaseTest):
method test_helpers_is_present (line 22) | def test_helpers_is_present(self):
method test_helpers_become_present (line 27) | def test_helpers_become_present(self):
FILE: tests/test_multisign.py
class TestMultisign (line 10) | class TestMultisign(IsignBaseTest):
method test_multisign (line 12) | def test_multisign(self):
FILE: tests/test_parsing.py
class TestParsing (line 9) | class TestParsing(IsignBaseTest):
method test_app (line 16) | def test_app(self):
FILE: tests/test_public_interface.py
class TestPublicInterface (line 10) | class TestPublicInterface(IsignBaseTest):
method _test_signable (line 12) | def _test_signable(self, filename, output_path):
method _test_unsignable (line 18) | def _test_unsignable(self, filename, output_path):
method test_app (line 23) | def test_app(self):
method test_app_ipa (line 26) | def test_app_ipa(self):
method test_app_with_frameworks_ipa (line 29) | def test_app_with_frameworks_ipa(self):
method test_appzip (line 32) | def test_appzip(self):
method test_non_app_txt (line 35) | def test_non_app_txt(self):
method test_non_app_ipa (line 38) | def test_non_app_ipa(self):
method test_simulator_app (line 41) | def test_simulator_app(self):
FILE: tests/test_sig.py
class TestSigner (line 6) | class TestSigner(IsignBaseTest):
method test_bad_signature (line 8) | def test_bad_signature(self):
FILE: tests/test_versioning.py
class VersioningTestCase (line 11) | class VersioningTestCase(unittest.TestCase):
method assert_proper_attribute (line 13) | def assert_proper_attribute(self, attribute):
method test_version_attribute (line 20) | def test_version_attribute(self):
method test_commit_attribute (line 32) | def test_commit_attribute(self):
method test_build_attribute (line 35) | def test_build_attribute(self):
FILE: tests/test_versus_apple.py
class TestVersusApple (line 19) | class TestVersusApple(IsignBaseTest):
method codesign_display (line 20) | def codesign_display(self, path):
method codesign_display_parse (line 35) | def codesign_display_parse(self, out):
method get_dict_with_key (line 106) | def get_dict_with_key(self, x, key):
method get_hashes_from_codesign_output_info (line 116) | def get_hashes_from_codesign_output_info(self, info):
method assert_common_signed_properties (line 122) | def assert_common_signed_properties(self, info):
method assert_common_signed_hashes (line 168) | def assert_common_signed_hashes(self, info, start_index, end_index):
method assert_hashes_for_signable (line 176) | def assert_hashes_for_signable(self, info, hashes_to_check):
method assert_matching_identifier (line 198) | def assert_matching_identifier(self, app_path, expected):
method check_bundle (line 203) | def check_bundle(self, path):
method check_dylib (line 211) | def check_dylib(self, path):
method test_override_identifier (line 216) | def test_override_identifier(self):
method test_app (line 264) | def test_app(self):
method test_app_from_scratch (line 301) | def test_app_from_scratch(self):
Condensed preview — 191 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (488K chars).
[
{
"path": ".gitignore",
"chars": 1490,
"preview": "# generic\n*~\n*#\n*._*\n*.log\n*.log.*\n*.pid\n\n# apple\n.DS_Store\n*~.nib\n\n# Byte-compiled / optimized / DLL files\n__pycache__/"
},
{
"path": "CONDUCT.md",
"chars": 4022,
"preview": "## Code of Conduct\n\n### What is this code of conduct for?\n\n`isign` is a piece of technology, but **the core of the `isig"
},
{
"path": "INSTALL.sh",
"chars": 7875,
"preview": "#!/bin/bash\n\nset -e\n\nREQUIRED_OPENSSL_VERSION=\"1.0.1\"\nREQUIRED_PIP_VERSION=\"7.0.0\"\nBREW_USER=\"unknown\"\nDEVELOP=false\n\nif"
},
{
"path": "LICENSE.txt",
"chars": 578,
"preview": "Copyright 2015 Sauce Labs\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this fi"
},
{
"path": "MANIFEST.in",
"chars": 116,
"preview": "include isign/version.json\ninclude isign/apple_credentials/applecerts.pem\ninclude isign/code_resources_template.xml\n"
},
{
"path": "PKG-INFO",
"chars": 28,
"preview": "Name: isign\nVersion: 1.6.16\n"
},
{
"path": "PREREQUISITES.rst",
"chars": 2464,
"preview": "Prerequisites\n=============\n\nFor Linux or Mac platforms, the ``INSTALL.sh`` script should take care of \neverything you n"
},
{
"path": "README.rst",
"chars": 8595,
"preview": "isign\n=====\n\nA tool and library to re-sign iOS applications, without proprietary Apple software.\n\nFor example, an iOS ap"
},
{
"path": "apple/README.rst",
"chars": 240,
"preview": "These scripts will re-provision (aka resign, code sign, etc) an app,\nusing Apple tools. The isign tools emulate what the"
},
{
"path": "apple/provisions.py",
"chars": 7207,
"preview": "#!/usr/bin/env python\n\n# Port of @mikehan's provisions.sh\n\nimport argparse\nimport glob\nimport os\nimport os.path\nimport s"
},
{
"path": "apple/provisions.sh",
"chars": 2230,
"preview": "#!/bin/bash\n\nusage() {\n echo \"./provisions -p [PATH_TO_NEW_PROVISIONING_PROFILE] -c \\\"CERT NAME: MUST BE IN KEYCHAIN\\"
},
{
"path": "bin/isign",
"chars": 6833,
"preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\nimport argparse\nfrom isign import isign\nfrom os.path import abspath, expan"
},
{
"path": "bin/isign_export_creds.sh",
"chars": 1037,
"preview": "#!/usr/bin/env bash\n\n# Export a certificate and key from a .p12 file into PEM form, and place\n# them where isign expects"
},
{
"path": "bin/isign_guess_mobileprovision.sh",
"chars": 2294,
"preview": "#!/usr/bin/env bash\n\n# given the filename certificate in PEM form, find potentially matching .mobileprovision files\n# in"
},
{
"path": "bin/make_seal",
"chars": 620,
"preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\nfrom optparse import OptionParser\nimport isign.code_resources\nimport loggi"
},
{
"path": "bin/multisign",
"chars": 3609,
"preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# From a single IPA, generate multiple re-signed IPAs simultaneously.\n# Wh"
},
{
"path": "bin/pprint_codesig",
"chars": 2061,
"preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\nimport argparse\nimport isign.archive\nfrom isign.exceptions import NotSigna"
},
{
"path": "dev/requirements.txt",
"chars": 18,
"preview": "nose\npytest\ntwine\n"
},
{
"path": "dev/setup.sh",
"chars": 194,
"preview": "#!/bin/bash\nset -e\n\n# determine local paths\npushd $(dirname \"$0\") >/dev/null\nDEV_DIR=$PWD\ncd ..\nSRC_DIR=$PWD\npopd >/dev/"
},
{
"path": "docs/applecerts.rst",
"chars": 1280,
"preview": "Apple certificates\n==================\n\nYou probably don't need to change this file, not for a long time.\n\nThe ``applecer"
},
{
"path": "docs/codedirectory.rst",
"chars": 1978,
"preview": "CodeDirectory slots\n===================\n\nA signature is mostly composed of hashes of blocks of the file's\ncontents. Howe"
},
{
"path": "docs/credentials.rst",
"chars": 6284,
"preview": "Apple developer credentials\n===========================\n\nMac OS X and iOS aren't like most other operating systems -- se"
},
{
"path": "docs/speed.rst",
"chars": 2665,
"preview": "So you want to make it go faster\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe *vast* majority of time in isign is not spent sig"
},
{
"path": "isign/__init__.py",
"chars": 295,
"preview": "# -*- coding: utf-8 -*-\nimport os.path\nimport json\n\npackage_dir = os.path.dirname(os.path.realpath(__file__))\nwith open("
},
{
"path": "isign/apple_credentials/README.rst",
"chars": 123,
"preview": "These Apple credentials are public knowledge (see the docs) so they \nare okay to distribute. Don't put anything else her"
},
{
"path": "isign/archive.py",
"chars": 15366,
"preview": "# -*- coding: utf-8 -*- \n\"\"\" Represents an app archive. This is an app at rest, whether it's a naked\n app bundle in a"
},
{
"path": "isign/bundle.py",
"chars": 11031,
"preview": "# -*- coding: utf-8 -*-\n\"\"\" Represents a bundle. In the words of the Apple docs, it's a convenient way to deliver\n so"
},
{
"path": "isign/code_resources.py",
"chars": 8734,
"preview": "# -*- coding: utf-8 -*-\nimport binascii\nimport copy\nimport hashlib\nimport logging\nfrom memoizer import memoize\nimport os"
},
{
"path": "isign/code_resources_template.xml",
"chars": 1696,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "isign/codesig.py",
"chars": 14116,
"preview": "# -*- coding: utf-8 -*-\nfrom abc import ABCMeta\nimport construct\nimport hashlib\nimport logging\nimport macho_cs\n\nimport u"
},
{
"path": "isign/der_encoder.py",
"chars": 1384,
"preview": "\nimport six\n\nfrom pyasn1.codec.der import encoder\nfrom pyasn1.type import univ\nfrom pyasn1.type import char\n\ndef encode("
},
{
"path": "isign/exceptions.py",
"chars": 681,
"preview": "# -*- coding: utf-8 -*-\n\"\"\" Some common exceptions \"\"\"\n\n\nclass NotSignable(Exception):\n \"\"\" superclass for any reason"
},
{
"path": "isign/isign.py",
"chars": 3118,
"preview": "# -*- coding: utf-8 -*-\nimport archive\n# import makesig\nimport exceptions\nimport os\nfrom os.path import dirname, exists,"
},
{
"path": "isign/macho.py",
"chars": 14986,
"preview": "# -*- coding: utf-8 -*-\n#\n# Constructs to represent various structures\n# in a Mach-O binary.\n#\n# As with all Constructs,"
},
{
"path": "isign/macho_cs.py",
"chars": 8133,
"preview": "# -*- coding: utf-8 -*-\n#\n# This is a Construct library which represents an\n# LC_CODE_SIGNATURE structure. Like all Cons"
},
{
"path": "isign/makesig.py",
"chars": 19346,
"preview": "# -*- coding: utf-8 -*-\n# Library to construct an LC_CODE_SIGNATURE construct\n# from scratch. Does not work yet.\n#\n# Aba"
},
{
"path": "isign/multisign.py",
"chars": 4497,
"preview": "# -*- coding: utf-8 -*-\nfrom os.path import isdir\nimport isign\nfrom archive import archive_factory\nfrom signer import Si"
},
{
"path": "isign/signable.py",
"chars": 11772,
"preview": "# -*- coding: utf-8 -*-\n#\n# Represents a file that can be signed. A file that\n# conforms to the Mach-O ABI.\n#\n# Executab"
},
{
"path": "isign/signer.py",
"chars": 6136,
"preview": "# -*- coding: utf-8 -*-\n#\n# Small object that can be passed around easily, that represents\n# our signing credentials, an"
},
{
"path": "isign/utils.py",
"chars": 612,
"preview": "# -*- coding: utf-8 -*- \nimport binascii\nimport re\n\n\ndef print_data(data):\n hexstring = binascii.hexlify(data)\n n "
},
{
"path": "jenkins.sh",
"chars": 1569,
"preview": "#!/bin/bash\n\n# figure out our package name from the name of repo\npushd $(dirname $0) >/dev/null\npackage_name=$(basename "
},
{
"path": "run_tests.sh",
"chars": 429,
"preview": "#!/bin/bash -e\nname=$(basename $PWD)\npackage=$(echo $name | sed 's/-/_/g')\n\n# look for required apps\nfor app in unzip zi"
},
{
"path": "setup.cfg",
"chars": 41,
"preview": "[metadata]\ndescription-file = README.rst\n"
},
{
"path": "setup.py",
"chars": 2053,
"preview": "# encoding: utf-8\nfrom setuptools import setup, find_packages\nfrom codecs import open # to use a consistent encoding\nfr"
},
{
"path": "tests/NotAnApp.txt",
"chars": 33,
"preview": "This file isn't actually an app.\n"
},
{
"path": "tests/NotAnAppDir/README.md",
"chars": 72,
"preview": "This is a directory which isign shouldn't accept as being an AppArchive\n"
},
{
"path": "tests/README.md",
"chars": 926,
"preview": "To test isign, we need to actually sign some apps. This directory contains\nthe tests and the test data AND the source fi"
},
{
"path": "tests/Test.app/PkgInfo",
"chars": 8,
"preview": "APPL????"
},
{
"path": "tests/Test.app/_CodeSignature/CodeResources",
"chars": 6909,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/Test.app/build.sh",
"chars": 1585,
"preview": "#!/bin/bash\n\n# ./build.sh /path/to/isign\n\n# builds various archival formats of the isign test app\n# and puts them in the"
},
{
"path": "tests/Test.app/isignTestApp.xcodeproj/project.pbxproj",
"chars": 19927,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 46;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "tests/Test.app/isignTestApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
"chars": 157,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"self:isignTestApp.xc"
},
{
"path": "tests/Test.app/isignTestApp.xcodeproj/xcuserdata/neilk.xcuserdatad/xcschemes/isignTestApp.xcscheme",
"chars": 4257,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n LastUpgradeVersion = \"0720\"\n version = \"1.3\">\n <BuildAction\n "
},
{
"path": "tests/Test.app/isignTestApp.xcodeproj/xcuserdata/neilk.xcuserdatad/xcschemes/xcschememanagement.plist",
"chars": 664,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/Test.app/isignTestAppTests/Info.plist",
"chars": 733,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/Test.app/isignTestAppTests/isignTestAppTests.swift",
"chars": 1000,
"preview": "//\n// isignTestAppTests.swift\n// isignTestAppTests\n//\n// Created by Neil Kandalgaonkar on 12/16/15.\n// Copyright © 2"
},
{
"path": "tests/Test.app/isignTestAppUITests/Info.plist",
"chars": 733,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/Test.app/isignTestAppUITests/isignTestAppUITests.swift",
"chars": 1263,
"preview": "//\n// isignTestAppUITests.swift\n// isignTestAppUITests\n//\n// Created by Neil Kandalgaonkar on 12/16/15.\n// Copyright"
},
{
"path": "tests/Test.app.codesig.construct.txt",
"chars": 9887,
"preview": "Container:\n cmd = 'LC_CODE_SIGNATURE'\n cmdsize = 16\n data = Container:\n dataoff = 71920\n datasize"
},
{
"path": "tests/Test_unsigned_fat.app/PkgInfo",
"chars": 8,
"preview": "APPL????"
},
{
"path": "tests/Test_unsigned_fat.app/build.sh",
"chars": 1585,
"preview": "#!/bin/bash\n\n# ./build.sh /path/to/isign\n\n# builds various archival formats of the isign test app\n# and puts them in the"
},
{
"path": "tests/Test_unsigned_fat.app/isignTestApp.xcodeproj/project.pbxproj",
"chars": 19927,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 46;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "tests/Test_unsigned_fat.app/isignTestApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
"chars": 157,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"self:isignTestApp.xc"
},
{
"path": "tests/Test_unsigned_fat.app/isignTestApp.xcodeproj/xcuserdata/neilk.xcuserdatad/xcschemes/isignTestApp.xcscheme",
"chars": 4257,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n LastUpgradeVersion = \"0720\"\n version = \"1.3\">\n <BuildAction\n "
},
{
"path": "tests/Test_unsigned_fat.app/isignTestApp.xcodeproj/xcuserdata/neilk.xcuserdatad/xcschemes/xcschememanagement.plist",
"chars": 664,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/Test_unsigned_fat.app/isignTestAppTests/Info.plist",
"chars": 733,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/Test_unsigned_fat.app/isignTestAppTests/isignTestAppTests.swift",
"chars": 1000,
"preview": "//\n// isignTestAppTests.swift\n// isignTestAppTests\n//\n// Created by Neil Kandalgaonkar on 12/16/15.\n// Copyright © 2"
},
{
"path": "tests/Test_unsigned_fat.app/isignTestAppUITests/Info.plist",
"chars": 733,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/Test_unsigned_fat.app/isignTestAppUITests/isignTestAppUITests.swift",
"chars": 1263,
"preview": "//\n// isignTestAppUITests.swift\n// isignTestAppUITests\n//\n// Created by Neil Kandalgaonkar on 12/16/15.\n// Copyright"
},
{
"path": "tests/Test_unsigned_thin.app/PkgInfo",
"chars": 8,
"preview": "APPL????"
},
{
"path": "tests/Test_unsigned_thin.app/build.sh",
"chars": 1585,
"preview": "#!/bin/bash\n\n# ./build.sh /path/to/isign\n\n# builds various archival formats of the isign test app\n# and puts them in the"
},
{
"path": "tests/Test_unsigned_thin.app/isignTestApp.xcodeproj/project.pbxproj",
"chars": 19927,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 46;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "tests/Test_unsigned_thin.app/isignTestApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
"chars": 157,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"self:isignTestApp.xc"
},
{
"path": "tests/Test_unsigned_thin.app/isignTestApp.xcodeproj/xcuserdata/neilk.xcuserdatad/xcschemes/isignTestApp.xcscheme",
"chars": 4257,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n LastUpgradeVersion = \"0720\"\n version = \"1.3\">\n <BuildAction\n "
},
{
"path": "tests/Test_unsigned_thin.app/isignTestApp.xcodeproj/xcuserdata/neilk.xcuserdatad/xcschemes/xcschememanagement.plist",
"chars": 664,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/Test_unsigned_thin.app/isignTestAppTests/Info.plist",
"chars": 733,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/Test_unsigned_thin.app/isignTestAppTests/isignTestAppTests.swift",
"chars": 1000,
"preview": "//\n// isignTestAppTests.swift\n// isignTestAppTests\n//\n// Created by Neil Kandalgaonkar on 12/16/15.\n// Copyright © 2"
},
{
"path": "tests/Test_unsigned_thin.app/isignTestAppUITests/Info.plist",
"chars": 733,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/Test_unsigned_thin.app/isignTestAppUITests/isignTestAppUITests.swift",
"chars": 1263,
"preview": "//\n// isignTestAppUITests.swift\n// isignTestAppUITests\n//\n// Created by Neil Kandalgaonkar on 12/16/15.\n// Copyright"
},
{
"path": "tests/bad_openssl",
"chars": 84,
"preview": "#!/bin/bash\n\n# returns nothing; simulates what happens with bad versions of openssl\n"
},
{
"path": "tests/credentials/README.rst",
"chars": 536,
"preview": "The test credentials in this directory cannot be used to sign apps for use on any iOS device. They\nare sufficiently simi"
},
{
"path": "tests/credentials/makeFakePprof.sh",
"chars": 155,
"preview": "#!/bin/bash\n\nopenssl smime -sign -in test.mobileprovision.plist -outform der -out test.mobileprovision -signer test.cert"
},
{
"path": "tests/credentials/test.cert.pem",
"chars": 1724,
"preview": "Bag Attributes\n friendlyName: isign_tests\n localKeyID: 25 67 EB F6 B5 12 B7 2F E7 38 49 10 87 2C 92 7D 0A 9D C2 63"
},
{
"path": "tests/credentials/test.key.pem",
"chars": 1829,
"preview": "Bag Attributes\n friendlyName: isign_tests\n localKeyID: 25 67 EB F6 B5 12 B7 2F E7 38 49 10 87 2C 92 7D 0A 9D C2 63"
},
{
"path": "tests/credentials/test.mobileprovision.plist",
"chars": 2721,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/credentials_std_names/README.rst",
"chars": 159,
"preview": "These credentials exist to test the 'credentials directory' feature in isign.\n\nThe credentials are copied from ../creden"
},
{
"path": "tests/credentials_std_names/certificate.pem",
"chars": 1724,
"preview": "Bag Attributes\n friendlyName: isign_tests\n localKeyID: 25 67 EB F6 B5 12 B7 2F E7 38 49 10 87 2C 92 7D 0A 9D C2 63"
},
{
"path": "tests/credentials_std_names_2/README.rst",
"chars": 183,
"preview": "These credentials exist to test the 'multisign' feature in isign\n\nThe credentials are copied from ../credentials_std_nam"
},
{
"path": "tests/credentials_std_names_2/certificate.pem",
"chars": 1724,
"preview": "Bag Attributes\n friendlyName: isign_tests\n localKeyID: 25 67 EB F6 B5 12 B7 2F E7 38 49 10 87 2C 92 7D 0A 9D C2 63"
},
{
"path": "tests/credentials_std_names_2/key.pem",
"chars": 1829,
"preview": "Bag Attributes\n friendlyName: isign_tests\n localKeyID: 25 67 EB F6 B5 12 B7 2F E7 38 49 10 87 2C 92 7D 0A 9D C2 63"
},
{
"path": "tests/generate_codesig_construct_txt.py",
"chars": 869,
"preview": "# generate a string representation of a signature parse for an app - do this to generate\n# test files such as Test.app.c"
},
{
"path": "tests/isignTestApp/.gitignore",
"chars": 6,
"preview": "build\n"
},
{
"path": "tests/isignTestApp/README.md",
"chars": 862,
"preview": "# isignTestApp\n\nProject to create a very simple test app for isign's test suite.\n\nRun `./build.sh` to create the necessa"
},
{
"path": "tests/isignTestApp/build.sh",
"chars": 2294,
"preview": "#!/bin/bash\n\n# ./build.sh /path/to/isign\n\n# builds various archival formats of the isign test app\n# and puts them in the"
},
{
"path": "tests/isignTestApp/exportOptions.plist",
"chars": 362,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/isignTestApp/isignTestApp/AppDelegate.swift",
"chars": 2648,
"preview": "//\n// AppDelegate.swift\n// isignTestApp\n//\n// Copyright © 2015 Sauce Labs.\n//\n// Licensed under the Apache License, "
},
{
"path": "tests/isignTestApp/isignTestApp/Assets.xcassets/AppIcon.appiconset/Contents.json",
"chars": 585,
"preview": "{\n \"images\" : [\n {\n \"idiom\" : \"iphone\",\n \"size\" : \"29x29\",\n \"scale\" : \"2x\"\n },\n {\n \"idiom\""
},
{
"path": "tests/isignTestApp/isignTestApp/Assets.xcassets/first.imageset/Contents.json",
"chars": 154,
"preview": "{\n \"images\" : [\n {\n \"idiom\" : \"universal\",\n \"filename\" : \"first.pdf\"\n }\n ],\n \"info\" : {\n \"version\""
},
{
"path": "tests/isignTestApp/isignTestApp/Assets.xcassets/second.imageset/Contents.json",
"chars": 155,
"preview": "{\n \"images\" : [\n {\n \"idiom\" : \"universal\",\n \"filename\" : \"second.pdf\"\n }\n ],\n \"info\" : {\n \"version"
},
{
"path": "tests/isignTestApp/isignTestApp/Base.lproj/LaunchScreen.storyboard",
"chars": 1664,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard"
},
{
"path": "tests/isignTestApp/isignTestApp/Base.lproj/Main.storyboard",
"chars": 9133,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard"
},
{
"path": "tests/isignTestApp/isignTestApp/FirstViewController.swift",
"chars": 1020,
"preview": "//\n// FirstViewController.swift\n// isignTestApp\n//\n// Copyright © 2015 Sauce Labs.\n//\n// Licensed under the Apache L"
},
{
"path": "tests/isignTestApp/isignTestApp/Info.plist",
"chars": 1404,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/isignTestApp/isignTestApp/SecondViewController.swift",
"chars": 1022,
"preview": "//\n// SecondViewController.swift\n// isignTestApp\n//\n// Copyright © 2015 Sauce Labs.\n//\n// Licensed under the Apache "
},
{
"path": "tests/isignTestApp/isignTestApp.xcodeproj/project.pbxproj",
"chars": 19927,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 46;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "tests/isignTestApp/isignTestApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
"chars": 157,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"self:isignTestApp.xc"
},
{
"path": "tests/isignTestApp/isignTestApp.xcodeproj/xcuserdata/neilk.xcuserdatad/xcschemes/isignTestApp.xcscheme",
"chars": 4257,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n LastUpgradeVersion = \"0720\"\n version = \"1.3\">\n <BuildAction\n "
},
{
"path": "tests/isignTestApp/isignTestApp.xcodeproj/xcuserdata/neilk.xcuserdatad/xcschemes/xcschememanagement.plist",
"chars": 664,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/isignTestApp/isignTestAppTests/Info.plist",
"chars": 733,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/isignTestApp/isignTestAppTests/isignTestAppTests.swift",
"chars": 1495,
"preview": "//\n// isignTestAppTests.swift\n// isignTestAppTests\n//\n// Copyright © 2015 Sauce Labs.\n//\n// Licensed under the Apach"
},
{
"path": "tests/isignTestApp/isignTestAppUITests/Info.plist",
"chars": 733,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/isignTestApp/isignTestAppUITests/isignTestAppUITests.swift",
"chars": 1758,
"preview": "//\n// isignTestAppUITests.swift\n// isignTestAppUITests\n//\n// Copyright © 2015 Sauce Labs.\n//\n// Licensed under the A"
},
{
"path": "tests/isignTestAppWithFrameworks/.gitignore",
"chars": 11,
"preview": "build\nPods\n"
},
{
"path": "tests/isignTestAppWithFrameworks/Podfile",
"chars": 226,
"preview": "source 'https://github.com/CocoaPods/Specs.git'\nplatform :ios, '8.0'\nuse_frameworks!\n\ntarget 'isignTestApp' do\n pod 'Fo"
},
{
"path": "tests/isignTestAppWithFrameworks/README.md",
"chars": 1436,
"preview": "# isignTestApp\n\nProject to create a very simple test app for isign's test suite.\n\nThis is exactly like the isignTestApp,"
},
{
"path": "tests/isignTestAppWithFrameworks/build.sh",
"chars": 1679,
"preview": "#!/bin/bash\n\n# ./build.sh /path/to/isign\n\n# builds various archival formats of the isign test app\n# and puts them in the"
},
{
"path": "tests/isignTestAppWithFrameworks/exportOptions.plist",
"chars": 413,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/isignTestAppWithFrameworks/isignTestApp/AppDelegate.swift",
"chars": 2648,
"preview": "//\n// AppDelegate.swift\n// isignTestApp\n//\n// Copyright © 2015 Sauce Labs.\n//\n// Licensed under the Apache License, "
},
{
"path": "tests/isignTestAppWithFrameworks/isignTestApp/Assets.xcassets/AppIcon.appiconset/Contents.json",
"chars": 585,
"preview": "{\n \"images\" : [\n {\n \"idiom\" : \"iphone\",\n \"size\" : \"29x29\",\n \"scale\" : \"2x\"\n },\n {\n \"idiom\""
},
{
"path": "tests/isignTestAppWithFrameworks/isignTestApp/Assets.xcassets/first.imageset/Contents.json",
"chars": 154,
"preview": "{\n \"images\" : [\n {\n \"idiom\" : \"universal\",\n \"filename\" : \"first.pdf\"\n }\n ],\n \"info\" : {\n \"version\""
},
{
"path": "tests/isignTestAppWithFrameworks/isignTestApp/Assets.xcassets/second.imageset/Contents.json",
"chars": 155,
"preview": "{\n \"images\" : [\n {\n \"idiom\" : \"universal\",\n \"filename\" : \"second.pdf\"\n }\n ],\n \"info\" : {\n \"version"
},
{
"path": "tests/isignTestAppWithFrameworks/isignTestApp/Base.lproj/LaunchScreen.storyboard",
"chars": 1664,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard"
},
{
"path": "tests/isignTestAppWithFrameworks/isignTestApp/Base.lproj/Main.storyboard",
"chars": 10483,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard"
},
{
"path": "tests/isignTestAppWithFrameworks/isignTestApp/FirstViewController.swift",
"chars": 1201,
"preview": "//\n// FirstViewController.swift\n// isignTestApp\n//\n// Copyright © 2015 Sauce Labs.\n//\n// Licensed under the Apache L"
},
{
"path": "tests/isignTestAppWithFrameworks/isignTestApp/Info.plist",
"chars": 1404,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/isignTestAppWithFrameworks/isignTestApp/SecondViewController.swift",
"chars": 1022,
"preview": "//\n// SecondViewController.swift\n// isignTestApp\n//\n// Copyright © 2015 Sauce Labs.\n//\n// Licensed under the Apache "
},
{
"path": "tests/isignTestAppWithFrameworks/isignTestApp.xcodeproj/project.pbxproj",
"chars": 23641,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 46;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "tests/isignTestAppWithFrameworks/isignTestApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
"chars": 157,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"self:isignTestApp.xc"
},
{
"path": "tests/isignTestAppWithFrameworks/isignTestApp.xcodeproj/xcuserdata/neilk.xcuserdatad/xcschemes/isignTestApp.xcscheme",
"chars": 4257,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n LastUpgradeVersion = \"0720\"\n version = \"1.3\">\n <BuildAction\n "
},
{
"path": "tests/isignTestAppWithFrameworks/isignTestApp.xcodeproj/xcuserdata/neilk.xcuserdatad/xcschemes/xcschememanagement.plist",
"chars": 664,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/isignTestAppWithFrameworks/isignTestApp.xcworkspace/contents.xcworkspacedata",
"chars": 230,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"group:isignTestApp.x"
},
{
"path": "tests/isignTestAppWithFrameworks/isignTestAppTests/Info.plist",
"chars": 733,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/isignTestAppWithFrameworks/isignTestAppTests/isignTestAppTests.swift",
"chars": 1495,
"preview": "//\n// isignTestAppTests.swift\n// isignTestAppTests\n//\n// Copyright © 2015 Sauce Labs.\n//\n// Licensed under the Apach"
},
{
"path": "tests/isignTestAppWithFrameworks/isignTestAppUITests/Info.plist",
"chars": 733,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/isignTestAppWithFrameworks/isignTestAppUITests/isignTestAppUITests.swift",
"chars": 1758,
"preview": "//\n// isignTestAppUITests.swift\n// isignTestAppUITests\n//\n// Copyright © 2015 Sauce Labs.\n//\n// Licensed under the A"
},
{
"path": "tests/isign_base_test.py",
"chars": 2679,
"preview": "from os.path import dirname, exists, join, isdir\nfrom isign import isign\nimport logging\nfrom monitor_temp_file import Mo"
},
{
"path": "tests/monitor_temp_file.py",
"chars": 2667,
"preview": "\"\"\"\"\nisign creates big temporary files, using the standard tempfile library.\nIf they are not cleaned up, they can fill u"
},
{
"path": "tests/sample-entitlements.plist",
"chars": 490,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "tests/test_archive.py",
"chars": 2721,
"preview": "from isign_base_test import IsignBaseTest\nfrom isign.archive import archive_factory, Archive, AppArchive, AppZipArchive,"
},
{
"path": "tests/test_creds_dir.py",
"chars": 1133,
"preview": "from isign.exceptions import MissingCredentials\nfrom isign_base_test import IsignBaseTest\nfrom isign import isign\nimport"
},
{
"path": "tests/test_entitlements.py",
"chars": 439,
"preview": "from isign_base_test import IsignBaseTest\nfrom isign.bundle import App\nimport logging\n\nlog = logging.getLogger(__name__)"
},
{
"path": "tests/test_helpers.py",
"chars": 1243,
"preview": "import isign.archive\nfrom isign.archive import AppZipArchive\nfrom isign.exceptions import MissingHelpers\nfrom isign_base"
},
{
"path": "tests/test_multisign.py",
"chars": 766,
"preview": "from isign_base_test import IsignBaseTest\nimport os\nfrom os.path import exists\nfrom isign.multisign import multisign\nimp"
},
{
"path": "tests/test_parsing.py",
"chars": 914,
"preview": "from isign_base_test import IsignBaseTest\nimport isign.bundle\nfrom isign.signable import Executable\nimport logging\n\nlog "
},
{
"path": "tests/test_public_interface.py",
"chars": 1352,
"preview": "from isign_base_test import IsignBaseTest\nimport os\nfrom os.path import exists\nfrom isign import isign\nimport logging\n\nl"
},
{
"path": "tests/test_sig.py",
"chars": 694,
"preview": "import isign\nfrom isign_base_test import IsignBaseTest\nfrom os.path import join\n\n\nclass TestSigner(IsignBaseTest):\n\n "
},
{
"path": "tests/test_versioning.py",
"chars": 1194,
"preview": "#!/usr/bin/env python\nimport os.path\nimport importlib\nimport unittest\n\ntests_dir = os.path.abspath(os.path.dirname(__fil"
},
{
"path": "tests/test_versus_apple.py",
"chars": 12519,
"preview": "from distutils import spawn\nfrom isign_base_test import IsignBaseTest\nimport logging\nfrom nose.plugins.skip import SkipT"
},
{
"path": "version.sh",
"chars": 1829,
"preview": "#!/bin/bash\n\n# Figures out what the current version is, echoes that back,\n# and also writes a `version.json` file into t"
}
]
// ... and 42 more files (download for full content)
About this extraction
This page contains the full source code of the apperian/isign GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 191 files (440.0 KB), approximately 127.5k tokens, and a symbol index with 265 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.