diff --git a/Squirrel.xcodeproj/project.pbxproj b/Squirrel.xcodeproj/project.pbxproj index 2594384..113a392 100644 --- a/Squirrel.xcodeproj/project.pbxproj +++ b/Squirrel.xcodeproj/project.pbxproj @@ -13,6 +13,8 @@ 534FF36717D8E90A0020A51A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534FF36517D8E90A0020A51A /* InfoPlist.strings */; }; 534FF36A17D8E90A0020A51A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 534FF36917D8E90A0020A51A /* main.m */; }; 534FF37317D8E9370020A51A /* com.github.Squirrel.TestApplication.TestService.xpc in Copy XPCServices */ = {isa = PBXBuildFile; fileRef = 534FF36017D8E90A0020A51A /* com.github.Squirrel.TestApplication.TestService.xpc */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 53710D7417D8F59700A992DE /* SQRLDeepCodesignSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 53710D7317D8F59700A992DE /* SQRLDeepCodesignSpec.m */; }; + 53710D8417D8F5CB00A992DE /* deep-codesign in Resources */ = {isa = PBXBuildFile; fileRef = 53710D7F17D8F5C300A992DE /* deep-codesign */; }; 5371815F18A29DC8005ED798 /* TestAppConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 5371815E18A29DC8005ED798 /* TestAppConstants.m */; }; 5371816018A29DC8005ED798 /* TestAppConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 5371815E18A29DC8005ED798 /* TestAppConstants.m */; }; 5374DCC6187AD0D8006B7056 /* SQRLAuthorization.h in Headers */ = {isa = PBXBuildFile; fileRef = 5374DCC4187AD0D8006B7056 /* SQRLAuthorization.h */; }; @@ -267,8 +269,10 @@ 534FF36617D8E90A0020A51A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 534FF36817D8E90A0020A51A /* TestService-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TestService-Prefix.pch"; sourceTree = ""; }; 534FF36917D8E90A0020A51A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 53710D7317D8F59700A992DE /* SQRLDeepCodesignSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLDeepCodesignSpec.m; sourceTree = ""; }; 53710D7D17D8F5C300A992DE /* bootstrap */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = bootstrap; sourceTree = ""; }; 53710D7E17D8F5C300A992DE /* cibuild */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = cibuild; sourceTree = ""; }; + 53710D7F17D8F5C300A992DE /* deep-codesign */ = {isa = PBXFileReference; lastKnownFileType = text; path = "deep-codesign"; sourceTree = ""; }; 53710D8017D8F5C300A992DE /* LICENSE.md */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE.md; sourceTree = ""; }; 53710D8117D8F5C300A992DE /* README.md */ = {isa = PBXFileReference; lastKnownFileType = text; path = README.md; sourceTree = ""; }; 53710D8217D8F5C300A992DE /* targets.awk */ = {isa = PBXFileReference; lastKnownFileType = text; path = targets.awk; sourceTree = ""; }; @@ -532,6 +536,7 @@ children = ( 53710D7D17D8F5C300A992DE /* bootstrap */, 53710D7E17D8F5C300A992DE /* cibuild */, + 53710D7F17D8F5C300A992DE /* deep-codesign */, 53710D8017D8F5C300A992DE /* LICENSE.md */, 53710D8117D8F5C300A992DE /* README.md */, 53710D8217D8F5C300A992DE /* targets.awk */, @@ -929,6 +934,7 @@ children = ( D00F5B8D17E82DE6009A4818 /* NSProcessInfoExtensionsSpec.m */, D0EDBEE017B0E3650058BC3C /* SQRLCodeSignatureSpec.m */, + 53710D7317D8F59700A992DE /* SQRLDeepCodesignSpec.m */, D049059A180554BA004E683E /* SQRLDirectoryManagerSpec.m */, D09D244017B595470001FAF8 /* SQRLInstallerSpec.m */, D049059C18055671004E683E /* SQRLShipItRequestSpec.m */, @@ -1137,6 +1143,7 @@ D08D4E4A17B451FD0012B22D /* TestApplication.app in Resources */, D09D244A17B59AB30001FAF8 /* TestApplication 2.1.app in Resources */, D000219717BAD34D0050109A /* TestApplication.app.zip in Resources */, + 53710D8417D8F5CB00A992DE /* deep-codesign in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1273,6 +1280,7 @@ D06B58D118035B5A00656D97 /* SQRLTerminationListenerSpec.m in Sources */, 5371815F18A29DC8005ED798 /* TestAppConstants.m in Sources */, D0EDBEE117B0E3650058BC3C /* SQRLCodeSignatureSpec.m in Sources */, + 53710D7417D8F59700A992DE /* SQRLDeepCodesignSpec.m in Sources */, D00F5B8E17E82DE6009A4818 /* NSProcessInfoExtensionsSpec.m in Sources */, D049059B180554BA004E683E /* SQRLDirectoryManagerSpec.m in Sources */, 5397A69D187DF8610014A477 /* SQRLInstallerOwnedBundle.m in Sources */, diff --git a/SquirrelTests/SQRLDeepCodesignSpec.m b/SquirrelTests/SQRLDeepCodesignSpec.m new file mode 100644 index 0000000..1cffd6c --- /dev/null +++ b/SquirrelTests/SQRLDeepCodesignSpec.m @@ -0,0 +1,126 @@ +// +// SQRLDeepCodesignSpec.m +// Squirrel +// +// Created by Keith Duncan on 2013-09-05. +// Copyright (c) 2013 GitHub. All rights reserved. +// + +#import +#import +#import +#import + +#import "QuickSpec+SQRLFixtures.h" + +QuickSpecBegin(SQRLDeepCodesign) + +NSMutableDictionary * (^environmentSuitableForChildProcess)(void) = ^ { + // Remove environment variables that configure the Obj-C runtime + // Specifically OBJC_DISABLE_GC so that child processes aren't forced to + // adopt the GC preference of the test suite + NSMutableDictionary *environment = [NSProcessInfo.processInfo.environment mutableCopy]; + NSSet *objcEnvironmentVariables = [environment keysOfEntriesPassingTest:^(NSString *variable, id obj, BOOL *stop) { + return [variable hasPrefix:@"OBJC"]; + }]; + [environment removeObjectsForKeys:objcEnvironmentVariables.allObjects]; + return environment; +}; + +NSTask * (^codesignTaskWithArguments)(NSArray *) = ^ (NSArray *arguments) { + NSTask *task = [[NSTask alloc] init]; + task.launchPath = @"/usr/bin/xcrun"; + task.arguments = [@[ @"codesign" ] arrayByAddingObjectsFromArray:arguments]; + task.environment = environmentSuitableForChildProcess(); + return task; +}; + +void (^resignTestApplicationPreserveEverythingButTheRequirements)(void) = ^{ + NSURL *testApplicationLocation = self.testApplicationURL; + + NSTask *resignCodesignTask = codesignTaskWithArguments(@[ + @"--sign", @"-", + @"--force", + @"--verbose=4", + @"--preserve-metadata=identifier,entitlements,resource-rules", + testApplicationLocation.path, + ]); + + [resignCodesignTask launch]; + [resignCodesignTask waitUntilExit]; + + expect(@(resignCodesignTask.terminationStatus)).to(equal(@0)); +}; + +void (^deepCodesignTestApplication)(void) = ^{ + NSURL *testApplicationLocation = self.testApplicationURL; + + NSBundle *testsBundle = [NSBundle bundleForClass:self.class]; + NSURL *deepCodesignLocation = [testsBundle URLForResource:@"deep-codesign" withExtension:nil]; + + NSNumber *executable = nil; + NSError *executableError = nil; + BOOL getExecutable = [deepCodesignLocation getResourceValue:&executable forKey:NSURLIsExecutableKey error:&executableError]; + expect(@(getExecutable)).to(beTruthy()); + expect(@(executable.boolValue)).to(beTruthy()); + expect(executableError).to(beNil()); + + NSTask *deepCodesignTask = [[NSTask alloc] init]; + deepCodesignTask.launchPath = deepCodesignLocation.path; + deepCodesignTask.standardError = [NSPipe pipe]; + deepCodesignTask.standardOutput = [NSPipe pipe]; + + NSMutableDictionary *environment = environmentSuitableForChildProcess(); + [environment addEntriesFromDictionary:@{ + @"CODE_SIGN_IDENTITY": @"-", + @"CONFIGURATION_BUILD_DIR": testApplicationLocation.URLByDeletingLastPathComponent.path, + @"FULL_PRODUCT_NAME": testApplicationLocation.lastPathComponent, + }]; + + deepCodesignTask.environment = environment; + + [deepCodesignTask launch]; + [deepCodesignTask waitUntilExit]; + + expect(@(deepCodesignTask.terminationStatus)).to(equal(@0)); + + /* + By signing test application's contents, which are covered by test + application's ResourceRules it's signature becomes invalid. Usually, + Xcode would sign the application after deep-codesign has run so the + signature would include the _signed_ child resources which are covered + by test application's ResourceRules + + test application is already signed, so we need to manually resign it + + deep-codesign preserves the requirements, we can't use it to resign the + root target, the new signing identity wouldn't verify against the + original designated requirement + */ + resignTestApplicationPreserveEverythingButTheRequirements(); +}; + +BOOL (^deepVerify)(void) = ^ BOOL { + NSTask *deepVerifyTask = codesignTaskWithArguments(@[ + @"--deep-verify", + @"--verbose=4", + self.testApplicationURL.path + ]); + + [deepVerifyTask launch]; + [deepVerifyTask waitUntilExit]; + + return deepVerifyTask.terminationStatus == 0; +}; + +it(@"should deep sign the test application", ^{ + deepCodesignTestApplication(); +}); + +xit(@"should deep verify after signing", ^{ + expect(@(deepVerify())).to(beFalsy()); + deepCodesignTestApplication(); + expect(@(deepVerify())).to(beTruthy()); +}); + +QuickSpecEnd diff --git a/script/deep-codesign b/script/deep-codesign new file mode 100755 index 0000000..fef1e3e --- /dev/null +++ b/script/deep-codesign @@ -0,0 +1,203 @@ +#!/usr/bin/env ruby + +def exit_error(description) + puts "{ \"error\": \"#{description}\" }" + exit 1 +end + +module Targets + class Object + def self.match(path) + !File.symlink?(path) + end + + attr_reader :path + + def initialize(path) + @path = path + end + + def search_directories + [] + end + end + + class Bundle < Object + def self.match(path) + return false unless super + + return false unless match = File.extname(path).match(/\.(.+)/) + return match[1] == extension + end + + def contents_directories + [ "Contents" ] + end + + def suffix_directories + [ + "Frameworks", + "SharedFrameworks", + "XPCServices", + "PlugIns", + "Support", + "SharedSupport", + File.join("Library", "LoginItems"), + File.join("Library", "LaunchServices"), + ] + end + + def search_directories + contents_directories.product(suffix_directories).map do |combination| + File.join(*combination) + end + end + end + + class VersionedBundle < Bundle + def contents_directories + contents_directories = [] + versions_directory = File.join(@path, "Versions") + Dir.foreach(versions_directory) do |current_version| + next if current_version.start_with?('.') + + full_path = File.join(versions_directory, current_version) + next if File.symlink?(full_path) + + contents_directories << File.join("Versions", current_version) + end + contents_directories + end + end + + class App < Bundle + def self.extension + "app" + end + end + + class Framework < VersionedBundle + def self.extension + "framework" + end + end + + class XPCService < Bundle + def self.extension + "xpc" + end + end + + class Tool < Object + def self.match(path) + return false unless super + return false unless File.executable?(path) + + # Look for an __TEXT,__info_plist section + # This contains the CFBundleIdentifier necessary for signing + `otool -l "#{path}"` =~ /__info_plist/ + end + end + + def self.all_targets + [ App, Framework, XPCService, Tool ] + end +end + +module Codesign + class Task + def self.sign(target, identity, timestamp) + arguments = [ "--sign", identity, "--force", "--preserve-metadata=identifier,entitlements,requirements", "--verbose", target.path ] + arguments.unshift("--timestamp=none") if not timestamp + codesign_with_arguments(arguments) + end + + protected + + def self.codesign_with_arguments(arguments) + $stdout.flush + + pid = fork + if pid.nil? + exec("/usr/bin/xcrun", *([ "codesign" ].concat(arguments))) + else + Process.wait(pid) + exit_error("codesign failed") unless $?.exitstatus == 0 + end + end + end +end + +codesign_identity = ENV['CODE_SIGN_IDENTITY'] +unless codesign_identity + exit_error("No codesign identity provided") +end + +build_dir = ENV['CONFIGURATION_BUILD_DIR'] +product_name = ENV['FULL_PRODUCT_NAME'] +unless build_dir && product_name + exit_error("Path to built product not provided") +end + +input_path = File.realpath(File.join(build_dir, product_name)) +unless File.exists?(input_path) + exit_error("Built product doesn't exist at path provided") +end + +targets = [ ] + +def match_path(path) + Targets.all_targets.each do |target| + return target.new(path) if target.match(path) + end + nil +end + +root_target = match_path input_path +if root_target.nil? + exit_error("Built product is not a known file structure, cannot deep codesign") +end + +# Recursion base case can't use build_dir because there can be >1 product +paths_to_search = root_target.search_directories.map do |current_dir| + File.join(root_target.path, current_dir) +end + +while paths_to_search.size > 0 + new_search_paths = [] + paths_to_search.each do |search_path| + next unless File.directory?(search_path) + + Dir.foreach(search_path) do |current_directory_entry| + next if current_directory_entry.start_with?(".") + File.delete(File.join(search_path, current_directory_entry)) if current_directory_entry.end_with?(".cstemp") + + target = match_path(File.join(search_path, current_directory_entry)) + next if target.nil? + + targets << target + + new_search_paths << target.search_directories.map do |current_search_directory| + File.join(target.path, current_search_directory) + end + end + end + paths_to_search = new_search_paths.flatten +end + +puts "Signing these targets:" +puts "="*22 +print "\n" + +# Sort into nested-most first, outer targets might include inner targets +# in their ResourceRules +targets = targets.sort_by do |target| + target.path.size +end +targets.reverse! + +use_timestamp = ENV["CONFIGURATION"] == "Release" +targets.each do |target| + puts "=> #{target.path}" + Codesign::Task.sign(target, codesign_identity, use_timestamp) +end