// CFBundleDisplayName // CFBundleName, fewer than 16 characters long // CFBundleIdentifier // http://developer.apple.com/library/ios/#documentation/general/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/20001431-102070 // Alphanumeric (A-Z,a-z,0-9), hyphen (-), and period (.) characters. // The string should also be in reverse-DNS format. void ReloadPList() { string Filename = Config.GetPlistOverrideFilename(); string SourcePList = ReadOrCreate(Filename); Utilities.PListHelper Helper = new Utilities.PListHelper(SourcePList); string BundleID; string BundleName; string BundleDisplayName; if (!Helper.GetString("CFBundleIdentifier", out BundleID)) { BundleID = "com.YourCompany.GameNameNoSpaces"; Helper.SetString("CFBundleIdentifier", BundleID); } if (!Helper.GetString("CFBundleName", out BundleName)) { BundleName = "MyUDKGame"; Helper.SetString("CFBundleName", BundleName); } if (!Helper.GetString("CFBundleDisplayName", out BundleDisplayName)) { BundleDisplayName = "UDK Game"; Helper.SetString("CFBundleDisplayName", BundleDisplayName); } BundleIdentifierEdit.Text = BundleID; BundleNameEdit.Text = BundleName; BundleDisplayNameEdit.Text = BundleDisplayName; }
/// <summary> /// Extracts the dict values for the Entitlements key and creates a new full .plist file /// from them (with outer plist and dict keys as well as doctype, etc...) /// </summary> public string GetEntitlementsString(string CFBundleIdentifier, out string TeamIdentifier) { Utilities.PListHelper XCentPList = null; Data.ProcessValueForKey("Entitlements", "dict", delegate(XmlNode ValueNode) { XCentPList = Utilities.PListHelper.CloneDictionaryRootedAt(ValueNode); }); // Modify the application-identifier to be fully qualified if needed string CurrentApplicationIdentifier; XCentPList.GetString("application-identifier", out CurrentApplicationIdentifier); XCentPList.GetString("com.apple.developer.team-identifier", out TeamIdentifier); // if (CurrentApplicationIdentifier.Contains("*")) { // Replace the application identifier string NewApplicationIdentifier = String.Format("{0}.{1}", ApplicationIdentifierPrefix, CFBundleIdentifier); XCentPList.SetString("application-identifier", NewApplicationIdentifier); // Replace the keychain access groups // Note: This isn't robust, it ignores the existing value in the wildcard and uses the same value for // each entry. If there is a legitimate need for more than one entry in the access group list, then // don't use a wildcard! List <string> KeyGroups = XCentPList.GetArray("keychain-access-groups", "string"); for (int i = 0; i < KeyGroups.Count; ++i) { string Entry = KeyGroups[i]; if (Entry.Contains("*")) { Entry = NewApplicationIdentifier; } KeyGroups[i] = Entry; } XCentPList.SetValueForKey("keychain-access-groups", KeyGroups); } return(XCentPList.SaveToString()); }
/// <summary> /// Extracts the dict values for the Entitlements key and creates a new full .plist file /// from them (with outer plist and dict keys as well as doctype, etc...) /// </summary> public string GetEntitlementsString(string CFBundleIdentifier, out string TeamIdentifier) { Utilities.PListHelper XCentPList = null; Data.ProcessValueForKey("Entitlements", "dict", delegate(XmlNode ValueNode) { XCentPList = Utilities.PListHelper.CloneDictionaryRootedAt(ValueNode); }); // Modify the application-identifier to be fully qualified if needed string CurrentApplicationIdentifier; XCentPList.GetString("application-identifier", out CurrentApplicationIdentifier); XCentPList.GetString("com.apple.developer.team-identifier", out TeamIdentifier); // if (CurrentApplicationIdentifier.Contains("*")) { // Replace the application identifier string NewApplicationIdentifier = String.Format("{0}.{1}", ApplicationIdentifierPrefix, CFBundleIdentifier); XCentPList.SetString("application-identifier", NewApplicationIdentifier); // Replace the keychain access groups // Note: This isn't robust, it ignores the existing value in the wildcard and uses the same value for // each entry. If there is a legitimate need for more than one entry in the access group list, then // don't use a wildcard! List <string> KeyGroups = XCentPList.GetArray("keychain-access-groups", "string"); for (int i = 0; i < KeyGroups.Count; ++i) { string Entry = KeyGroups[i]; if (Entry.Contains("*")) { Entry = NewApplicationIdentifier; } KeyGroups[i] = Entry; } XCentPList.SetValueForKey("keychain-access-groups", KeyGroups); } // must have CloudKit and CloudDocuments for com.apple.developer.icloud-services // otherwise the game will not be listed in the Settings->iCloud apps menu on the device { // iOS only if (Platform == "IOS" && XCentPList.HasKey("com.apple.developer.icloud-services")) { List <string> ServicesGroups = XCentPList.GetArray("com.apple.developer.icloud-services", "string"); ServicesGroups.Clear(); ServicesGroups.Add("CloudKit"); ServicesGroups.Add("CloudDocuments"); XCentPList.SetValueForKey("com.apple.developer.icloud-services", ServicesGroups); } // For distribution builds, the entitlements from mobileprovisioning have a modified syntax if (Config.bForDistribution) { // remove the wildcards from the ubiquity-kvstore-identifier string if (XCentPList.HasKey("com.apple.developer.ubiquity-kvstore-identifier")) { string UbiquityKvstoreString; XCentPList.GetString("com.apple.developer.ubiquity-kvstore-identifier", out UbiquityKvstoreString); int DotPosition = UbiquityKvstoreString.LastIndexOf("*"); if (DotPosition >= 0) { string TeamPrefix = DotPosition > 1 ? UbiquityKvstoreString.Substring(0, DotPosition - 1) : TeamIdentifier; string NewUbiquityKvstoreIdentifier = String.Format("{0}.{1}", TeamPrefix, CFBundleIdentifier); XCentPList.SetValueForKey("com.apple.developer.ubiquity-kvstore-identifier", NewUbiquityKvstoreIdentifier); } } // remove the wildcards from the ubiquity-container-identifiers array if (XCentPList.HasKey("com.apple.developer.ubiquity-container-identifiers")) { List <string> UbiquityContainerIdentifiersGroups = XCentPList.GetArray("com.apple.developer.ubiquity-container-identifiers", "string"); for (int i = 0; i < UbiquityContainerIdentifiersGroups.Count; i++) { int DotPosition = UbiquityContainerIdentifiersGroups[i].LastIndexOf("*"); if (DotPosition >= 0) { string TeamPrefix = DotPosition > 1 ? UbiquityContainerIdentifiersGroups[i].Substring(0, DotPosition - 1) : TeamIdentifier; string NewUbiquityContainerIdentifier = String.Format("{0}.{1}", TeamPrefix, CFBundleIdentifier); UbiquityContainerIdentifiersGroups[i] = NewUbiquityContainerIdentifier; } } if (UbiquityContainerIdentifiersGroups.Count == 0) { string NewUbiquityKvstoreIdentifier = String.Format("{0}.{1}", TeamIdentifier, CFBundleIdentifier); UbiquityContainerIdentifiersGroups.Add(NewUbiquityKvstoreIdentifier); } XCentPList.SetValueForKey("com.apple.developer.ubiquity-container-identifiers", UbiquityContainerIdentifiersGroups); } // remove the wildcards from the developer.associated-domains array or string if (XCentPList.HasKey("com.apple.developer.associated-domains")) { string AssociatedDomainsString; XCentPList.GetString("com.apple.developer.associated-domains", out AssociatedDomainsString); //check if the value is string if (AssociatedDomainsString != null && AssociatedDomainsString.Contains("*")) { XCentPList.RemoveKeyValue("com.apple.developer.associated-domains"); } else { //check if the value is an array List <string> AssociatedDomainsGroup = XCentPList.GetArray("com.apple.developer.associated-domains", "string"); if (AssociatedDomainsGroup.Count == 1 && AssociatedDomainsGroup[0].Contains("*")) { XCentPList.RemoveKeyValue("com.apple.developer.associated-domains"); } } } // remove development keys - generated when the cloudkit container is in development mode XCentPList.RemoveKeyValue("com.apple.developer.icloud-container-development-container-identifiers"); } // set the icloud-container-environment according to the project settings if (XCentPList.HasKey("com.apple.developer.icloud-container-environment")) { List <string> ContainerEnvironmentGroup = XCentPList.GetArray("com.apple.developer.icloud-container-environment", "string"); if (ContainerEnvironmentGroup.Count != 0) { ContainerEnvironmentGroup.Clear(); // The new value is a string, not an array string NewContainerEnvironment = Config.bForDistribution ? "Production" : "Development"; XCentPList.SetValueForKey("com.apple.developer.icloud-container-environment", NewContainerEnvironment); } } } return(XCentPList.SaveToString()); }
/// <summary> /// Does the actual work of signing the application /// Modifies the following files: /// Info.plist /// [Executable] (file name derived from CFBundleExecutable in the Info.plist, e.g., UDKGame) /// _CodeSignature/CodeResources /// [ResourceRules] (file name derived from CFBundleResourceSpecification, e.g., CustomResourceRules.plist) /// </summary> public void PerformSigning() { DateTime SigningTime = DateTime.Now; // Get the name of the executable file string CFBundleExecutable; if (!Info.GetString("CFBundleExecutable", out CFBundleExecutable)) { throw new InvalidDataException("Info.plist must contain the key CFBundleExecutable"); } // Get the name of the bundle string CFBundleIdentifier; if (!Info.GetString("CFBundleIdentifier", out CFBundleIdentifier)) { throw new InvalidDataException("Info.plist must contain the key CFBundleIdentifier"); } // Verify there is a resource rules file and make a dummy one if needed. // If it's missing, CreateCodeResourceDirectory can't proceed (the Info.plist is already written to disk at that point) if (!Info.HasKey("CFBundleResourceSpecification")) { // Couldn't find the key, create a dummy one string CFBundleResourceSpecification = "CustomResourceRules.plist"; Info.SetString("CFBundleResourceSpecification", CFBundleResourceSpecification); Program.Warning("Info.plist was missing the key CFBundleResourceSpecification, creating a new resource rules file '{0}'.", CFBundleResourceSpecification); } // Save the Info.plist out byte[] RawInfoPList = Encoding.UTF8.GetBytes(Info.SaveToString()); Info.SetReadOnly(true); FileSystem.WriteAllBytes("Info.plist", RawInfoPList); Program.Log(" ... Writing updated Info.plist"); // Create the code resources file and load it byte[] ResourceDirBytes = CreateCodeResourcesDirectory(CFBundleExecutable); // Open the executable Program.Log("Opening source executable..."); byte[] SourceExeData = FileSystem.ReadAllBytes(CFBundleExecutable); FatBinaryFile FatBinary = new FatBinaryFile(); FatBinary.LoadFromBytes(SourceExeData); //@TODO: Verify it's an executable (not an object file, etc...) ulong CurrentStreamOffset = 0; byte[] FinalExeData = new byte[SourceExeData.Length + 1024 * 1024]; int ArchIndex = 0; foreach (MachObjectFile Exe in FatBinary.MachObjectFiles) { Program.Log("... Processing one mach object (binary is {0})", FatBinary.bIsFatBinary ? "fat" : "thin"); // Pad the memory stream with extra room to handle any possible growth in the code signing data int OverSize = 1024 * 1024; int ExeSize = (FatBinary.bIsFatBinary ? (int)FatBinary.Archs[ArchIndex].Size : SourceExeData.Length); MemoryStream OutputExeStream = new MemoryStream(ExeSize + OverSize); // Copy the data up to the executable into the final stream if (FatBinary.bIsFatBinary) { if (ArchIndex == 0) { OutputExeStream.Seek(0, SeekOrigin.Begin); OutputExeStream.Write(SourceExeData, (int)CurrentStreamOffset, (int)FatBinary.Archs[ArchIndex].Offset - (int)CurrentStreamOffset); OutputExeStream.Seek(0, SeekOrigin.Begin); byte[] HeaderData = OutputExeStream.ToArray(); HeaderData.CopyTo(FinalExeData, (long)CurrentStreamOffset); CurrentStreamOffset += (ulong)HeaderData.Length; } else { byte[] ZeroData = new byte[(int)FatBinary.Archs[ArchIndex].Offset - (int)CurrentStreamOffset]; ZeroData.CopyTo(FinalExeData, (long)CurrentStreamOffset); CurrentStreamOffset += (ulong)ZeroData.Length; } } // Copy the executable into the stream int ExeOffset = (FatBinary.bIsFatBinary ? (int)FatBinary.Archs[ArchIndex].Offset : 0); OutputExeStream.Seek(0, SeekOrigin.Begin); OutputExeStream.Write(SourceExeData, ExeOffset, ExeSize); OutputExeStream.Seek(0, SeekOrigin.Begin); long Length = OutputExeStream.Length; // Find out if there was an existing code sign blob and find the linkedit segment command MachLoadCommandCodeSignature CodeSigningBlobLC = null; MachLoadCommandSegment LinkEditSegmentLC = null; foreach (MachLoadCommand Command in Exe.Commands) { if (CodeSigningBlobLC == null) { CodeSigningBlobLC = Command as MachLoadCommandCodeSignature; } if (LinkEditSegmentLC == null) { LinkEditSegmentLC = Command as MachLoadCommandSegment; if (LinkEditSegmentLC.SegmentName != "__LINKEDIT") { LinkEditSegmentLC = null; } } } if (LinkEditSegmentLC == null) { throw new InvalidDataException("Did not find a Mach segment load command for the __LINKEDIT segment"); } // If the existing code signing blob command is missing, make sure there is enough space to add it // Insert the code signing blob if it isn't present //@TODO: Insert the code signing blob if it isn't present if (CodeSigningBlobLC == null) { throw new InvalidDataException("Did not find a Code Signing LC. Injecting one into a fresh executable is not currently supported."); } // Verify that the code signing blob is at the end of the linkedit segment (and thus can be expanded if needed) if ((CodeSigningBlobLC.BlobFileOffset + CodeSigningBlobLC.BlobFileSize) != (LinkEditSegmentLC.FileOffset + LinkEditSegmentLC.FileSize)) { throw new InvalidDataException("Code Signing LC was present but not at the end of the __LINKEDIT segment, unable to replace it"); } int SignedFileLength = (int)CodeSigningBlobLC.BlobFileOffset; // Create the code directory blob CodeDirectoryBlob FinalCodeDirectoryBlob = CodeDirectoryBlob.Create(CFBundleIdentifier, SignedFileLength); // Create the entitlements blob string EntitlementsText = BuildEntitlementString(CFBundleIdentifier); EntitlementsBlob FinalEntitlementsBlob = EntitlementsBlob.Create(EntitlementsText); // Create or preserve the requirements blob RequirementsBlob FinalRequirementsBlob = null; if ((CodeSigningBlobLC != null) && Config.bMaintainExistingRequirementsWhenCodeSigning) { RequirementsBlob OldRequirements = CodeSigningBlobLC.Payload.GetBlobByMagic(AbstractBlob.CSMAGIC_REQUIREMENTS_TABLE) as RequirementsBlob; FinalRequirementsBlob = OldRequirements; } if (FinalRequirementsBlob == null) { FinalRequirementsBlob = RequirementsBlob.CreateEmpty(); } // Create the code signature blob (which actually signs the code directory) CodeDirectorySignatureBlob CodeSignatureBlob = CodeDirectorySignatureBlob.Create(); // Create the code signature superblob (which contains all of the other signature-related blobs) CodeSigningTableBlob CodeSignPayload = CodeSigningTableBlob.Create(); CodeSignPayload.Add(0x00000, FinalCodeDirectoryBlob); CodeSignPayload.Add(0x00002, FinalRequirementsBlob); CodeSignPayload.Add(0x00005, FinalEntitlementsBlob); CodeSignPayload.Add(0x10000, CodeSignatureBlob); // The ordering of the following steps (and doing the signature twice below) must be preserved. // The reason is there are some chicken-and-egg issues here: // The code directory stores a hash of the header, but // The header stores the size of the __LINKEDIT section, which is where the signature blobs go, but // The CMS signature blob signs the code directory // // So, we need to know the size of a signature blob in order to write a header that is itself hashed // and signed by the signature blob // Do an initial signature just to get the size Program.Log("... Initial signature step ({0:0.00} s elapsed so far)", (DateTime.Now - SigningTime).TotalSeconds); CodeSignatureBlob.SignCodeDirectory(SigningCert, SigningTime, FinalCodeDirectoryBlob); // Compute the size of everything, and push it into the EXE header byte[] DummyPayload = CodeSignPayload.GetBlobBytes(); // Adjust the header and load command to have the correct size for the code sign blob WritingContext OutputExeContext = new WritingContext(new BinaryWriter(OutputExeStream)); long BlobLength = DummyPayload.Length; long NonCodeSigSize = (long)LinkEditSegmentLC.FileSize - CodeSigningBlobLC.BlobFileSize; long BlobStartPosition = NonCodeSigSize + (long)LinkEditSegmentLC.FileOffset; LinkEditSegmentLC.PatchFileLength(OutputExeContext, (uint)(NonCodeSigSize + BlobLength)); CodeSigningBlobLC.PatchPositionAndSize(OutputExeContext, (uint)BlobStartPosition, (uint)BlobLength); // Now that the executable loader command has been inserted and the appropriate section modified, compute all the hashes Program.Log("... Computing hashes ({0:0.00} s elapsed so far)", (DateTime.Now - SigningTime).TotalSeconds); OutputExeContext.Flush(); // Fill out the special hashes FinalCodeDirectoryBlob.GenerateSpecialSlotHash(CodeDirectoryBlob.cdInfoSlot, RawInfoPList); FinalCodeDirectoryBlob.GenerateSpecialSlotHash(CodeDirectoryBlob.cdRequirementsSlot, FinalRequirementsBlob.GetBlobBytes()); FinalCodeDirectoryBlob.GenerateSpecialSlotHash(CodeDirectoryBlob.cdResourceDirSlot, ResourceDirBytes); FinalCodeDirectoryBlob.GenerateSpecialSlotHash(CodeDirectoryBlob.cdApplicationSlot); FinalCodeDirectoryBlob.GenerateSpecialSlotHash(CodeDirectoryBlob.cdEntitlementSlot, FinalEntitlementsBlob.GetBlobBytes()); // Fill out the regular hashes FinalCodeDirectoryBlob.ComputeImageHashes(OutputExeStream.ToArray()); // And compute the final signature Program.Log("... Final signature step ({0:0.00} s elapsed so far)", (DateTime.Now - SigningTime).TotalSeconds); CodeSignatureBlob.SignCodeDirectory(SigningCert, SigningTime, FinalCodeDirectoryBlob); // Generate the signing blob and place it in the output (verifying it didn't change in size) byte[] FinalPayload = CodeSignPayload.GetBlobBytes(); if (DummyPayload.Length != FinalPayload.Length) { throw new InvalidDataException("CMS signature blob changed size between practice run and final run, unable to create useful code signing data"); } OutputExeContext.PushPositionAndJump(BlobStartPosition); OutputExeContext.Write(FinalPayload); OutputExeContext.PopPosition(); // Truncate the data so the __LINKEDIT section extends right to the end Program.Log("... Committing all edits ({0:0.00} s elapsed so far)", (DateTime.Now - SigningTime).TotalSeconds); OutputExeContext.CompleteWritingAndClose(); Program.Log("... Truncating/copying final binary", DateTime.Now - SigningTime); ulong DesiredExecutableLength = LinkEditSegmentLC.FileSize + LinkEditSegmentLC.FileOffset; if ((ulong)Length < DesiredExecutableLength) { throw new InvalidDataException("Data written is smaller than expected, unable to finish signing process"); } byte[] Data = OutputExeStream.ToArray(); Data.CopyTo(FinalExeData, (long)CurrentStreamOffset); CurrentStreamOffset += DesiredExecutableLength; // update the header if it is a fat binary if (FatBinary.bIsFatBinary) { FatBinary.Archs[ArchIndex].Size = (uint)DesiredExecutableLength; } // increment the architecture index ArchIndex++; } // re-write the header FatBinary.WriteHeader(ref FinalExeData, 0); // resize to the finale size Array.Resize(ref FinalExeData, (int)CurrentStreamOffset); //@todo: Extend the file system interface so we don't have to copy 20 MB just to truncate a few hundred bytes // Save the patched and signed executable Program.Log("Saving signed executable... ({0:0.00} s elapsed so far)", (DateTime.Now - SigningTime).TotalSeconds); FileSystem.WriteAllBytes(CFBundleExecutable, FinalExeData); Program.Log("Finished code signing, which took {0:0.00} s", (DateTime.Now - SigningTime).TotalSeconds); }
private void SaveChanges() { // Development settings { // Open the existing plist override file string DevFilename = Config.GetPlistOverrideFilename(false); string SourcePList = ReadOrCreate(DevFilename); Utilities.PListHelper Helper = new Utilities.PListHelper(SourcePList); // Jam the edited values in if (!Helper.HasKey("UIFileSharingEnabled")) { Helper.SetValueForKey("UIFileSharingEnabled", true); } Helper.SetString("CFBundleIdentifier", BundleIdentifierEdit.Text); Helper.SetString("CFBundleName", BundleNameEdit.Text); Helper.SetString("CFBundleDisplayName", BundleDisplayNameEdit.Text); // Save the modified plist SavePList(Helper, DevFilename); } // Distribution settings { // Open the existing plist override file string DistFilename = Config.GetPlistOverrideFilename(true); string SourcePList = ReadOrCreate(DistFilename); Utilities.PListHelper Helper = new Utilities.PListHelper(SourcePList); // Jam the edited values in if (!Helper.HasKey("UIFileSharingEnabled")) { Helper.SetValueForKey("UIFileSharingEnabled", false); } // Save the modified plist SavePList(Helper, DistFilename); } }