Browse Source

[macOS export] Implements ad-hoc signing on Linux/Windows, adds extra privacy settings, entitlements warnings and error checking.

bruvzg 4 years ago
parent
commit
8bb00a2dfd

+ 2 - 5
misc/dist/osx_template.app/Contents/Info.plist

@@ -24,10 +24,7 @@
 	<string>$signature</string>
 	<key>CFBundleVersion</key>
 	<string>$version</string>
-	<key>NSMicrophoneUsageDescription</key>
-	<string>$microphone_usage_description</string>
-	<key>NSCameraUsageDescription</key>
-	<string>$camera_usage_description</string>
+$usage_descriptions
 	<key>NSHumanReadableCopyright</key>
 	<string>$copyright</string>
 	<key>CFBundleSupportedPlatforms</key>
@@ -46,6 +43,6 @@
 		<string>10.12</string>
 	</dict>
 	<key>NSHighResolutionCapable</key>
-	$highres
+$highres
 </dict>
 </plist>

+ 1564 - 0
platform/osx/export/codesign.cpp

@@ -0,0 +1,1564 @@
+/*************************************************************************/
+/*  codesign.cpp                                                         */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#include "codesign.h"
+
+#include "lipo.h"
+#include "macho.h"
+#include "plist.h"
+
+#include "core/os/os.h"
+#include "editor/editor_settings.h"
+#include "modules/modules_enabled.gen.h" // For regex.
+
+#include <ctime>
+
+#ifdef MODULE_REGEX_ENABLED
+
+/*************************************************************************/
+/* CodeSignCodeResources                                                 */
+/*************************************************************************/
+
+String CodeSignCodeResources::hash_sha1_base64(const String &p_path) {
+	FileAccessRef fa = FileAccess::open(p_path, FileAccess::READ);
+	ERR_FAIL_COND_V_MSG(!fa, String(), vformat("CodeSign/CodeResources: Can't open file: \"%s\".", p_path));
+
+	CryptoCore::SHA1Context ctx;
+	ctx.start();
+
+	unsigned char step[4096];
+	while (true) {
+		uint64_t br = fa->get_buffer(step, 4096);
+		if (br > 0) {
+			ctx.update(step, br);
+		}
+		if (br < 4096) {
+			break;
+		}
+	}
+
+	unsigned char hash[0x14];
+	ctx.finish(hash);
+	fa->close();
+
+	return CryptoCore::b64_encode_str(hash, 0x14);
+}
+
+String CodeSignCodeResources::hash_sha256_base64(const String &p_path) {
+	FileAccessRef fa = FileAccess::open(p_path, FileAccess::READ);
+	ERR_FAIL_COND_V_MSG(!fa, String(), vformat("CodeSign/CodeResources: Can't open file: \"%s\".", p_path));
+
+	CryptoCore::SHA256Context ctx;
+	ctx.start();
+
+	unsigned char step[4096];
+	while (true) {
+		uint64_t br = fa->get_buffer(step, 4096);
+		if (br > 0) {
+			ctx.update(step, br);
+		}
+		if (br < 4096) {
+			break;
+		}
+	}
+
+	unsigned char hash[0x20];
+	ctx.finish(hash);
+	fa->close();
+
+	return CryptoCore::b64_encode_str(hash, 0x20);
+}
+
+void CodeSignCodeResources::add_rule1(const String &p_rule, const String &p_key, int p_weight, bool p_store) {
+	rules1.push_back(CRRule(p_rule, p_key, p_weight, p_store));
+}
+
+void CodeSignCodeResources::add_rule2(const String &p_rule, const String &p_key, int p_weight, bool p_store) {
+	rules2.push_back(CRRule(p_rule, p_key, p_weight, p_store));
+}
+
+CodeSignCodeResources::CRMatch CodeSignCodeResources::match_rules1(const String &p_path) const {
+	CRMatch found = CRMatch::CR_MATCH_NO;
+	int weight = 0;
+	for (int i = 0; i < rules1.size(); i++) {
+		RegEx regex = RegEx(rules1[i].file_pattern);
+		if (regex.search(p_path).is_valid()) {
+			if (rules1[i].key == "omit") {
+				return CRMatch::CR_MATCH_NO;
+			} else if (rules1[i].key == "nested") {
+				if (weight <= rules1[i].weight) {
+					found = CRMatch::CR_MATCH_NESTED;
+					weight = rules1[i].weight;
+				}
+			} else if (rules1[i].key == "optional") {
+				if (weight <= rules1[i].weight) {
+					found = CRMatch::CR_MATCH_OPTIONAL;
+					weight = rules1[i].weight;
+				}
+			} else {
+				if (weight <= rules1[i].weight) {
+					found = CRMatch::CR_MATCH_YES;
+					weight = rules1[i].weight;
+				}
+			}
+		}
+	}
+	return found;
+}
+
+CodeSignCodeResources::CRMatch CodeSignCodeResources::match_rules2(const String &p_path) const {
+	CRMatch found = CRMatch::CR_MATCH_NO;
+	int weight = 0;
+	for (int i = 0; i < rules2.size(); i++) {
+		RegEx regex = RegEx(rules2[i].file_pattern);
+		if (regex.search(p_path).is_valid()) {
+			if (rules2[i].key == "omit") {
+				return CRMatch::CR_MATCH_NO;
+			} else if (rules2[i].key == "nested") {
+				if (weight <= rules2[i].weight) {
+					found = CRMatch::CR_MATCH_NESTED;
+					weight = rules2[i].weight;
+				}
+			} else if (rules2[i].key == "optional") {
+				if (weight <= rules2[i].weight) {
+					found = CRMatch::CR_MATCH_OPTIONAL;
+					weight = rules2[i].weight;
+				}
+			} else {
+				if (weight <= rules2[i].weight) {
+					found = CRMatch::CR_MATCH_YES;
+					weight = rules2[i].weight;
+				}
+			}
+		}
+	}
+	return found;
+}
+
+bool CodeSignCodeResources::add_file1(const String &p_root, const String &p_path) {
+	CRMatch found = match_rules1(p_path);
+	if (found != CRMatch::CR_MATCH_YES && found != CRMatch::CR_MATCH_OPTIONAL) {
+		return true; // No match.
+	}
+
+	CRFile f;
+	f.name = p_path;
+	f.optional = (found == CRMatch::CR_MATCH_OPTIONAL);
+	f.nested = false;
+	f.hash = hash_sha1_base64(p_root.plus_file(p_path));
+	print_verbose(vformat("CodeSign/CodeResources: File(V1) %s hash1:%s", f.name, f.hash));
+
+	files1.push_back(f);
+	return true;
+}
+
+bool CodeSignCodeResources::add_file2(const String &p_root, const String &p_path) {
+	CRMatch found = match_rules2(p_path);
+	if (found == CRMatch::CR_MATCH_NESTED) {
+		return add_nested_file(p_root, p_path, p_root.plus_file(p_path));
+	}
+	if (found != CRMatch::CR_MATCH_YES && found != CRMatch::CR_MATCH_OPTIONAL) {
+		return true; // No match.
+	}
+
+	CRFile f;
+	f.name = p_path;
+	f.optional = (found == CRMatch::CR_MATCH_OPTIONAL);
+	f.nested = false;
+	f.hash = hash_sha1_base64(p_root.plus_file(p_path));
+	f.hash2 = hash_sha256_base64(p_root.plus_file(p_path));
+
+	print_verbose(vformat("CodeSign/CodeResources: File(V2) %s hash1:%s hash2:%s", f.name, f.hash, f.hash2));
+
+	files2.push_back(f);
+	return true;
+}
+
+bool CodeSignCodeResources::add_nested_file(const String &p_root, const String &p_path, const String &p_exepath) {
+#define CLEANUP()                                       \
+	if (files_to_add.size() > 1) {                      \
+		for (int j = 0; j < files_to_add.size(); j++) { \
+			da->remove(files_to_add[j]);                \
+		}                                               \
+	}
+
+	DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+	ERR_FAIL_COND_V(!da, false);
+
+	Vector<String> files_to_add;
+	if (LipO::is_lipo(p_exepath)) {
+		String tmp_path_name = EditorPaths::get_singleton()->get_cache_dir().plus_file("_lipo");
+		Error err = da->make_dir_recursive(tmp_path_name);
+		if (err != OK) {
+			ERR_FAIL_V_MSG(false, vformat("CodeSign/CodeResources: Failed to create \"%s\" subfolder.", tmp_path_name));
+		}
+		LipO lip;
+		if (lip.open_file(p_exepath)) {
+			for (int i = 0; i < lip.get_arch_count(); i++) {
+				if (!lip.extract_arch(i, tmp_path_name.plus_file("_rqexe_" + itos(i)))) {
+					CLEANUP();
+					ERR_FAIL_V_MSG(false, "CodeSign/CodeResources: Failed to extract thin binary.");
+				}
+				files_to_add.push_back(tmp_path_name.plus_file("_rqexe_" + itos(i)));
+			}
+		}
+	} else if (MachO::is_macho(p_exepath)) {
+		files_to_add.push_back(p_exepath);
+	}
+
+	CRFile f;
+	f.name = p_path;
+	f.optional = false;
+	f.nested = true;
+	for (int i = 0; i < files_to_add.size(); i++) {
+		MachO mh;
+		if (!mh.open_file(files_to_add[i])) {
+			CLEANUP();
+			ERR_FAIL_V_MSG(false, "CodeSign/CodeResources: Invalid executable file.");
+		}
+		PackedByteArray hash = mh.get_cdhash_sha256(); // Use SHA-256 variant, if available.
+		if (hash.size() != 0x20) {
+			hash = mh.get_cdhash_sha1(); // Use SHA-1 instead.
+			if (hash.size() != 0x14) {
+				CLEANUP();
+				ERR_FAIL_V_MSG(false, "CodeSign/CodeResources: Unsigned nested executable file.");
+			}
+		}
+		hash.resize(0x14); // Always clamp to 0x14 size.
+		f.hash = CryptoCore::b64_encode_str(hash.ptr(), hash.size());
+
+		PackedByteArray rq_blob = mh.get_requirements();
+		String req_string;
+		if (rq_blob.size() > 8) {
+			CodeSignRequirements rq = CodeSignRequirements(rq_blob);
+			Vector<String> rqs = rq.parse_requirements();
+			for (int j = 0; j < rqs.size(); j++) {
+				if (rqs[j].begins_with("designated => ")) {
+					req_string = rqs[j].replace("designated => ", "");
+				}
+			}
+		}
+		if (req_string.is_empty()) {
+			req_string = "cdhash H\"" + String::hex_encode_buffer(hash.ptr(), hash.size()) + "\"";
+		}
+		print_verbose(vformat("CodeSign/CodeResources: Nested object %s (cputype: %d) cdhash:%s designated rq:%s", f.name, mh.get_cputype(), f.hash, req_string));
+		if (f.requirements != req_string) {
+			if (i != 0) {
+				f.requirements += " or ";
+			}
+			f.requirements += req_string;
+		}
+	}
+	files2.push_back(f);
+
+	CLEANUP();
+	return true;
+
+#undef CLEANUP
+}
+
+bool CodeSignCodeResources::add_folder_recursive(const String &p_root, const String &p_path, const String &p_main_exe_path) {
+	DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+	ERR_FAIL_COND_V(!da, false);
+	Error err = da->change_dir(p_root.plus_file(p_path));
+	ERR_FAIL_COND_V(err != OK, false);
+
+	bool ret = true;
+	da->list_dir_begin();
+	String n = da->get_next();
+	while (n != String()) {
+		if (n != "." && n != "..") {
+			String path = p_root.plus_file(p_path).plus_file(n);
+			if (path == p_main_exe_path) {
+				n = da->get_next();
+				continue; // Skip main executable.
+			}
+			if (da->current_is_dir()) {
+				CRMatch found = match_rules2(p_path.plus_file(n));
+				String fmw_ver = "Current"; // Framework version (default).
+				String info_path;
+				String main_exe;
+				bool bundle = false;
+				if (da->file_exists(path.plus_file("Contents/Info.plist"))) {
+					info_path = path.plus_file("Contents/Info.plist");
+					main_exe = path.plus_file("Contents/MacOS");
+					bundle = true;
+				} else if (da->file_exists(path.plus_file(vformat("Versions/%s/Resources/Info.plist", fmw_ver)))) {
+					info_path = path.plus_file(vformat("Versions/%s/Resources/Info.plist", fmw_ver));
+					main_exe = path.plus_file(vformat("Versions/%s", fmw_ver));
+					bundle = true;
+				} else if (da->file_exists(path.plus_file("Info.plist"))) {
+					info_path = path.plus_file("Info.plist");
+					main_exe = path;
+					bundle = true;
+				}
+				if (bundle && found == CRMatch::CR_MATCH_NESTED && !info_path.is_empty()) {
+					// Read Info.plist.
+					PList info_plist;
+					if (info_plist.load_file(info_path)) {
+						if (info_plist.get_root()->data_type == PList::PLNodeType::PL_NODE_TYPE_DICT && info_plist.get_root()->data_dict.has("CFBundleExecutable")) {
+							main_exe = main_exe.plus_file(String::utf8(info_plist.get_root()->data_dict["CFBundleExecutable"]->data_string.get_data()));
+						} else {
+							ERR_FAIL_V_MSG(false, "CodeSign/CodeResources: Invalid Info.plist, no exe name.");
+						}
+					} else {
+						ERR_FAIL_V_MSG(false, "CodeSign/CodeResources: Invalid Info.plist, can't load.");
+					}
+					ret = ret && add_nested_file(p_root, p_path.plus_file(n), main_exe);
+				} else {
+					ret = ret && add_folder_recursive(p_root, p_path.plus_file(n), p_main_exe_path);
+				}
+			} else {
+				ret = ret && add_file1(p_root, p_path.plus_file(n));
+				ret = ret && add_file2(p_root, p_path.plus_file(n));
+			}
+		}
+
+		n = da->get_next();
+	}
+
+	da->list_dir_end();
+	return ret;
+}
+
+bool CodeSignCodeResources::save_to_file(const String &p_path) {
+	PList pl;
+
+	print_verbose(vformat("CodeSign/CodeResources: Writing to file: %s", p_path));
+
+	// Write version 1 hashes.
+	Ref<PListNode> files1_dict = PListNode::new_dict();
+	pl.get_root()->push_subnode(files1_dict, "files");
+	for (int i = 0; i < files1.size(); i++) {
+		if (files1[i].optional) {
+			Ref<PListNode> file_dict = PListNode::new_dict();
+			files1_dict->push_subnode(file_dict, files1[i].name);
+
+			file_dict->push_subnode(PListNode::new_data(files1[i].hash), "hash");
+			file_dict->push_subnode(PListNode::new_bool(true), "optional");
+		} else {
+			files1_dict->push_subnode(PListNode::new_data(files1[i].hash), files1[i].name);
+		}
+	}
+
+	// Write version 2 hashes.
+	Ref<PListNode> files2_dict = PListNode::new_dict();
+	pl.get_root()->push_subnode(files2_dict, "files2");
+	for (int i = 0; i < files2.size(); i++) {
+		Ref<PListNode> file_dict = PListNode::new_dict();
+		files2_dict->push_subnode(file_dict, files2[i].name);
+
+		if (files2[i].nested) {
+			file_dict->push_subnode(PListNode::new_data(files2[i].hash), "cdhash");
+			file_dict->push_subnode(PListNode::new_string(files2[i].requirements), "requirement");
+		} else {
+			file_dict->push_subnode(PListNode::new_data(files2[i].hash), "hash");
+			file_dict->push_subnode(PListNode::new_data(files2[i].hash2), "hash2");
+			if (files2[i].optional) {
+				file_dict->push_subnode(PListNode::new_bool(true), "optional");
+			}
+		}
+	}
+
+	// Write version 1 rules.
+	Ref<PListNode> rules1_dict = PListNode::new_dict();
+	pl.get_root()->push_subnode(rules1_dict, "rules");
+	for (int i = 0; i < rules1.size(); i++) {
+		if (rules1[i].store) {
+			if (rules1[i].key.is_empty() && rules1[i].weight <= 0) {
+				rules1_dict->push_subnode(PListNode::new_bool(true), rules1[i].file_pattern);
+			} else {
+				Ref<PListNode> rule_dict = PListNode::new_dict();
+				rules1_dict->push_subnode(rule_dict, rules1[i].file_pattern);
+				if (!rules1[i].key.is_empty()) {
+					rule_dict->push_subnode(PListNode::new_bool(true), rules1[i].key);
+				}
+				if (rules1[i].weight != 1) {
+					rule_dict->push_subnode(PListNode::new_real(rules1[i].weight), "weight");
+				}
+			}
+		}
+	}
+
+	// Write version 2 rules.
+	Ref<PListNode> rules2_dict = PListNode::new_dict();
+	pl.get_root()->push_subnode(rules2_dict, "rules2");
+	for (int i = 0; i < rules2.size(); i++) {
+		if (rules2[i].store) {
+			if (rules2[i].key.is_empty() && rules2[i].weight <= 0) {
+				rules2_dict->push_subnode(PListNode::new_bool(true), rules2[i].file_pattern);
+			} else {
+				Ref<PListNode> rule_dict = PListNode::new_dict();
+				rules2_dict->push_subnode(rule_dict, rules2[i].file_pattern);
+				if (!rules2[i].key.is_empty()) {
+					rule_dict->push_subnode(PListNode::new_bool(true), rules2[i].key);
+				}
+				if (rules2[i].weight != 1) {
+					rule_dict->push_subnode(PListNode::new_real(rules2[i].weight), "weight");
+				}
+			}
+		}
+	}
+	String text = pl.save_text();
+	ERR_FAIL_COND_V_MSG(text.is_empty(), false, "CodeSign/CodeResources: Generating resources PList failed.");
+
+	FileAccessRef fa = FileAccess::open(p_path, FileAccess::WRITE);
+	ERR_FAIL_COND_V_MSG(!fa, false, vformat("CodeSign/CodeResources: Can't open file: \"%s\".", p_path));
+
+	CharString cs = text.utf8();
+	fa->store_buffer((const uint8_t *)cs.ptr(), cs.length());
+	fa->close();
+	return true;
+}
+
+/*************************************************************************/
+/* CodeSignRequirements                                                  */
+/*************************************************************************/
+
+CodeSignRequirements::CodeSignRequirements() {
+	blob.append_array({ 0xFA, 0xDE, 0x0C, 0x01 }); // Requirement set magic.
+	blob.append_array({ 0x00, 0x00, 0x00, 0x0C }); // Length of requirements set (12 bytes).
+	blob.append_array({ 0x00, 0x00, 0x00, 0x00 }); // Empty.
+}
+
+CodeSignRequirements::CodeSignRequirements(const PackedByteArray &p_data) {
+	blob = p_data;
+}
+
+_FORCE_INLINE_ void CodeSignRequirements::_parse_certificate_slot(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const {
+#define _R(x) BSWAP32(*(uint32_t *)(blob.ptr() + x))
+	ERR_FAIL_COND_MSG(r_pos >= p_rq_size, "CodeSign/Requirements: Out of bounds.");
+	r_out += "certificate ";
+	uint32_t tag_slot = _R(r_pos);
+	if (tag_slot == 0x00000000) {
+		r_out += "leaf";
+	} else if (tag_slot == 0xffffffff) {
+		r_out += "root";
+	} else {
+		r_out += itos((int32_t)tag_slot);
+	}
+	r_pos += 4;
+#undef _R
+}
+
+_FORCE_INLINE_ void CodeSignRequirements::_parse_key(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const {
+#define _R(x) BSWAP32(*(uint32_t *)(blob.ptr() + x))
+	ERR_FAIL_COND_MSG(r_pos >= p_rq_size, "CodeSign/Requirements: Out of bounds.");
+	uint32_t key_size = _R(r_pos);
+	ERR_FAIL_COND_MSG(r_pos + key_size > p_rq_size, "CodeSign/Requirements: Out of bounds.");
+	CharString key;
+	key.resize(key_size);
+	memcpy(key.ptrw(), blob.ptr() + r_pos + 4, key_size);
+	r_pos += 4 + key_size + PAD(key_size, 4);
+	r_out += "[" + String::utf8(key, key_size) + "]";
+#undef _R
+}
+
+_FORCE_INLINE_ void CodeSignRequirements::_parse_oid_key(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const {
+#define _R(x) BSWAP32(*(uint32_t *)(blob.ptr() + x))
+	ERR_FAIL_COND_MSG(r_pos >= p_rq_size, "CodeSign/Requirements: Out of bounds.");
+	uint32_t key_size = _R(r_pos);
+	ERR_FAIL_COND_MSG(r_pos + key_size > p_rq_size, "CodeSign/Requirements: Out of bounds.");
+	r_out += "[field.";
+	r_out += itos(blob[r_pos + 4] / 40) + ".";
+	r_out += itos(blob[r_pos + 4] % 40);
+	uint32_t spos = r_pos + 5;
+	while (spos < r_pos + 4 + key_size) {
+		r_out += ".";
+		if (blob[spos] <= 127) {
+			r_out += itos(blob[spos]);
+			spos += 1;
+		} else {
+			uint32_t x = (0x7F & blob[spos]) << 7;
+			spos += 1;
+			while (blob[spos] > 127) {
+				x = (x + (0x7F & blob[spos])) << 7;
+				spos += 1;
+			}
+			x = (x + (0x7F & blob[spos]));
+			r_out += itos(x);
+			spos += 1;
+		}
+	}
+	r_out += "]";
+	r_pos += 4 + key_size + PAD(key_size, 4);
+#undef _R
+}
+
+_FORCE_INLINE_ void CodeSignRequirements::_parse_hash_string(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const {
+#define _R(x) BSWAP32(*(uint32_t *)(blob.ptr() + x))
+	ERR_FAIL_COND_MSG(r_pos >= p_rq_size, "CodeSign/Requirements: Out of bounds.");
+	uint32_t tag_size = _R(r_pos);
+	ERR_FAIL_COND_MSG(r_pos + tag_size > p_rq_size, "CodeSign/Requirements: Out of bounds.");
+	PackedByteArray data;
+	data.resize(tag_size);
+	memcpy(data.ptrw(), blob.ptr() + r_pos + 4, tag_size);
+	r_out += "H\"" + String::hex_encode_buffer(data.ptr(), data.size()) + "\"";
+	r_pos += 4 + tag_size + PAD(tag_size, 4);
+#undef _R
+}
+
+_FORCE_INLINE_ void CodeSignRequirements::_parse_value(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const {
+#define _R(x) BSWAP32(*(uint32_t *)(blob.ptr() + x))
+	ERR_FAIL_COND_MSG(r_pos >= p_rq_size, "CodeSign/Requirements: Out of bounds.");
+	uint32_t key_size = _R(r_pos);
+	ERR_FAIL_COND_MSG(r_pos + key_size > p_rq_size, "CodeSign/Requirements: Out of bounds.");
+	CharString key;
+	key.resize(key_size);
+	memcpy(key.ptrw(), blob.ptr() + r_pos + 4, key_size);
+	r_pos += 4 + key_size + PAD(key_size, 4);
+	r_out += "\"" + String::utf8(key, key_size) + "\"";
+#undef _R
+}
+
+_FORCE_INLINE_ void CodeSignRequirements::_parse_date(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const {
+#define _R(x) BSWAP32(*(uint32_t *)(blob.ptr() + x))
+	ERR_FAIL_COND_MSG(r_pos >= p_rq_size, "CodeSign/Requirements: Out of bounds.");
+	uint32_t date = _R(r_pos);
+	time_t t = 978307200 + date;
+	struct tm lt;
+#ifdef WINDOWS_ENABLED
+	gmtime_s(&lt, &t);
+#else
+	gmtime_r(&t, &lt);
+#endif
+	r_out += vformat("<%04d-%02d-%02d ", (int)(1900 + lt.tm_year), (int)(lt.tm_mon + 1), (int)(lt.tm_mday)) + vformat("%02d:%02d:%02d +0000>", (int)(lt.tm_hour), (int)(lt.tm_min), (int)(lt.tm_sec));
+#undef _R
+}
+
+_FORCE_INLINE_ bool CodeSignRequirements::_parse_match(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const {
+#define _R(x) BSWAP32(*(uint32_t *)(blob.ptr() + x))
+	ERR_FAIL_COND_V_MSG(r_pos >= p_rq_size, false, "CodeSign/Requirements: Out of bounds.");
+	uint32_t match = _R(r_pos);
+	r_pos += 4;
+	switch (match) {
+		case 0x00000000: {
+			r_out += "exists";
+		} break;
+		case 0x00000001: {
+			r_out += "= ";
+			_parse_value(r_pos, r_out, p_rq_size);
+		} break;
+		case 0x00000002: {
+			r_out += "~ ";
+			_parse_value(r_pos, r_out, p_rq_size);
+		} break;
+		case 0x00000003: {
+			r_out += "= *";
+			_parse_value(r_pos, r_out, p_rq_size);
+		} break;
+		case 0x00000004: {
+			r_out += "= ";
+			_parse_value(r_pos, r_out, p_rq_size);
+			r_out += "*";
+		} break;
+		case 0x00000005: {
+			r_out += "< ";
+			_parse_value(r_pos, r_out, p_rq_size);
+		} break;
+		case 0x00000006: {
+			r_out += "> ";
+			_parse_value(r_pos, r_out, p_rq_size);
+		} break;
+		case 0x00000007: {
+			r_out += "<= ";
+			_parse_value(r_pos, r_out, p_rq_size);
+		} break;
+		case 0x00000008: {
+			r_out += ">= ";
+			_parse_value(r_pos, r_out, p_rq_size);
+		} break;
+		case 0x00000009: {
+			r_out += "= ";
+			_parse_date(r_pos, r_out, p_rq_size);
+		} break;
+		case 0x0000000A: {
+			r_out += "< ";
+			_parse_date(r_pos, r_out, p_rq_size);
+		} break;
+		case 0x0000000B: {
+			r_out += "> ";
+			_parse_date(r_pos, r_out, p_rq_size);
+		} break;
+		case 0x0000000C: {
+			r_out += "<= ";
+			_parse_date(r_pos, r_out, p_rq_size);
+		} break;
+		case 0x0000000D: {
+			r_out += ">= ";
+			_parse_date(r_pos, r_out, p_rq_size);
+		} break;
+		case 0x0000000E: {
+			r_out += "absent";
+		} break;
+		default: {
+			return false;
+		}
+	}
+	return true;
+#undef _R
+}
+
+Vector<String> CodeSignRequirements::parse_requirements() const {
+#define _R(x) BSWAP32(*(uint32_t *)(blob.ptr() + x))
+	Vector<String> list;
+
+	// Read requirements set header.
+	ERR_FAIL_COND_V_MSG(blob.size() < 12, list, "CodeSign/Requirements: Blob is too small.");
+	uint32_t magic = _R(0);
+	ERR_FAIL_COND_V_MSG(magic != 0xfade0c01, list, "CodeSign/Requirements: Invalid set magic.");
+	uint32_t size = _R(4);
+	ERR_FAIL_COND_V_MSG(size != (uint32_t)blob.size(), list, "CodeSign/Requirements: Invalid set size.");
+	uint32_t count = _R(8);
+
+	for (uint32_t i = 0; i < count; i++) {
+		String out;
+
+		// Read requirement header.
+		uint32_t rq_type = _R(12 + i * 8);
+		uint32_t rq_offset = _R(12 + i * 8 + 4);
+		ERR_FAIL_COND_V_MSG(rq_offset + 12 >= (uint32_t)blob.size(), list, "CodeSign/Requirements: Invalid requirement offset.");
+		switch (rq_type) {
+			case 0x00000001: {
+				out += "host => ";
+			} break;
+			case 0x00000002: {
+				out += "guest => ";
+			} break;
+			case 0x00000003: {
+				out += "designated => ";
+			} break;
+			case 0x00000004: {
+				out += "library => ";
+			} break;
+			case 0x00000005: {
+				out += "plugin => ";
+			} break;
+			default: {
+				ERR_FAIL_V_MSG(list, "CodeSign/Requirements: Invalid requirement type.");
+			}
+		}
+		uint32_t rq_magic = _R(rq_offset);
+		uint32_t rq_size = _R(rq_offset + 4);
+		uint32_t rq_ver = _R(rq_offset + 8);
+		uint32_t pos = rq_offset + 12;
+		ERR_FAIL_COND_V_MSG(rq_magic != 0xfade0c00, list, "CodeSign/Requirements: Invalid requirement magic.");
+		ERR_FAIL_COND_V_MSG(rq_ver != 0x00000001, list, "CodeSign/Requirements: Invalid requirement version.");
+
+		// Read requirement tokens.
+		List<String> tokens;
+		while (pos < rq_offset + rq_size) {
+			uint32_t rq_tag = _R(pos);
+			pos += 4;
+			String token;
+			switch (rq_tag) {
+				case 0x00000000: {
+					token = "false";
+				} break;
+				case 0x00000001: {
+					token = "true";
+				} break;
+				case 0x00000002: {
+					token = "identifier ";
+					_parse_value(pos, token, rq_offset + rq_size);
+				} break;
+				case 0x00000003: {
+					token = "anchor apple";
+				} break;
+				case 0x00000004: {
+					_parse_certificate_slot(pos, token, rq_offset + rq_size);
+					token += " ";
+					_parse_hash_string(pos, token, rq_offset + rq_size);
+				} break;
+				case 0x00000005: {
+					token = "info";
+					_parse_key(pos, token, rq_offset + rq_size);
+					token += " = ";
+					_parse_value(pos, token, rq_offset + rq_size);
+				} break;
+				case 0x00000006: {
+					token = "and";
+				} break;
+				case 0x00000007: {
+					token = "or";
+				} break;
+				case 0x00000008: {
+					token = "cdhash ";
+					_parse_hash_string(pos, token, rq_offset + rq_size);
+				} break;
+				case 0x00000009: {
+					token = "!";
+				} break;
+				case 0x0000000A: {
+					token = "info";
+					_parse_key(pos, token, rq_offset + rq_size);
+					token += " ";
+					ERR_FAIL_COND_V_MSG(!_parse_match(pos, token, rq_offset + rq_size), list, "CodeSign/Requirements: Unsupported match suffix.");
+				} break;
+				case 0x0000000B: {
+					_parse_certificate_slot(pos, token, rq_offset + rq_size);
+					_parse_key(pos, token, rq_offset + rq_size);
+					token += " ";
+					ERR_FAIL_COND_V_MSG(!_parse_match(pos, token, rq_offset + rq_size), list, "CodeSign/Requirements: Unsupported match suffix.");
+				} break;
+				case 0x0000000C: {
+					_parse_certificate_slot(pos, token, rq_offset + rq_size);
+					token += " trusted";
+				} break;
+				case 0x0000000D: {
+					token = "anchor trusted";
+				} break;
+				case 0x0000000E: {
+					_parse_certificate_slot(pos, token, rq_offset + rq_size);
+					_parse_oid_key(pos, token, rq_offset + rq_size);
+					token += " ";
+					ERR_FAIL_COND_V_MSG(!_parse_match(pos, token, rq_offset + rq_size), list, "CodeSign/Requirements: Unsupported match suffix.");
+				} break;
+				case 0x0000000F: {
+					token = "anchor apple generic";
+				} break;
+				default: {
+					ERR_FAIL_V_MSG(list, "CodeSign/Requirements: Invalid requirement token.");
+				} break;
+			}
+			tokens.push_back(token);
+		}
+
+		// Polish to infix notation (w/o bracket optimization).
+		for (List<String>::Element *E = tokens.back(); E; E = E->prev()) {
+			if (E->get() == "and") {
+				ERR_FAIL_COND_V_MSG(!E->next() || !E->next()->next(), list, "CodeSign/Requirements: Invalid token sequence.");
+				String token = "(" + E->next()->get() + " and " + E->next()->next()->get() + ")";
+				tokens.erase(E->next()->next());
+				tokens.erase(E->next());
+				E->get() = token;
+			} else if (E->get() == "or") {
+				ERR_FAIL_COND_V_MSG(!E->next() || !E->next()->next(), list, "CodeSign/Requirements: Invalid token sequence.");
+				String token = "(" + E->next()->get() + " or " + E->next()->next()->get() + ")";
+				tokens.erase(E->next()->next());
+				tokens.erase(E->next());
+				E->get() = token;
+			}
+		}
+
+		if (tokens.size() == 1) {
+			list.push_back(out + tokens.front()->get());
+		} else {
+			ERR_FAIL_V_MSG(list, "CodeSign/Requirements: Invalid token sequence.");
+		}
+	}
+
+	return list;
+#undef _R
+}
+
+PackedByteArray CodeSignRequirements::get_hash_sha1() const {
+	PackedByteArray hash;
+	hash.resize(0x14);
+
+	CryptoCore::SHA1Context ctx;
+	ctx.start();
+	ctx.update(blob.ptr(), blob.size());
+	ctx.finish(hash.ptrw());
+
+	return hash;
+}
+
+PackedByteArray CodeSignRequirements::get_hash_sha256() const {
+	PackedByteArray hash;
+	hash.resize(0x20);
+
+	CryptoCore::SHA256Context ctx;
+	ctx.start();
+	ctx.update(blob.ptr(), blob.size());
+	ctx.finish(hash.ptrw());
+
+	return hash;
+}
+
+int CodeSignRequirements::get_size() const {
+	return blob.size();
+}
+
+void CodeSignRequirements::write_to_file(FileAccess *p_file) const {
+	ERR_FAIL_COND_MSG(!p_file, "CodeSign/Requirements: Invalid file handle.");
+	p_file->store_buffer(blob.ptr(), blob.size());
+}
+
+/*************************************************************************/
+/* CodeSignEntitlementsText                                             */
+/*************************************************************************/
+
+CodeSignEntitlementsText::CodeSignEntitlementsText() {
+	blob.append_array({ 0xFA, 0xDE, 0x71, 0x71 }); // Text Entitlements set magic.
+	blob.append_array({ 0x00, 0x00, 0x00, 0x08 }); // Length (8 bytes).
+}
+
+CodeSignEntitlementsText::CodeSignEntitlementsText(const String &p_string) {
+	CharString utf8 = p_string.utf8();
+	blob.append_array({ 0xFA, 0xDE, 0x71, 0x71 }); // Text Entitlements set magic.
+	for (int i = 3; i >= 0; i--) {
+		uint8_t x = ((utf8.length() + 8) >> i * 8) & 0xFF; // Size.
+		blob.push_back(x);
+	}
+	for (int i = 0; i < utf8.length(); i++) { // Write data.
+		blob.push_back(utf8[i]);
+	}
+}
+
+PackedByteArray CodeSignEntitlementsText::get_hash_sha1() const {
+	PackedByteArray hash;
+	hash.resize(0x14);
+
+	CryptoCore::SHA1Context ctx;
+	ctx.start();
+	ctx.update(blob.ptr(), blob.size());
+	ctx.finish(hash.ptrw());
+
+	return hash;
+}
+
+PackedByteArray CodeSignEntitlementsText::get_hash_sha256() const {
+	PackedByteArray hash;
+	hash.resize(0x20);
+
+	CryptoCore::SHA256Context ctx;
+	ctx.start();
+	ctx.update(blob.ptr(), blob.size());
+	ctx.finish(hash.ptrw());
+
+	return hash;
+}
+
+int CodeSignEntitlementsText::get_size() const {
+	return blob.size();
+}
+
+void CodeSignEntitlementsText::write_to_file(FileAccess *p_file) const {
+	ERR_FAIL_COND_MSG(!p_file, "CodeSign/EntitlementsText: Invalid file handle.");
+	p_file->store_buffer(blob.ptr(), blob.size());
+}
+
+/*************************************************************************/
+/* CodeSignEntitlementsBinary                                           */
+/*************************************************************************/
+
+CodeSignEntitlementsBinary::CodeSignEntitlementsBinary() {
+	blob.append_array({ 0xFA, 0xDE, 0x71, 0x72 }); // Binary Entitlements magic.
+	blob.append_array({ 0x00, 0x00, 0x00, 0x08 }); // Length (8 bytes).
+}
+
+CodeSignEntitlementsBinary::CodeSignEntitlementsBinary(const String &p_string) {
+	PList pl = PList(p_string);
+
+	PackedByteArray asn1 = pl.save_asn1();
+	blob.append_array({ 0xFA, 0xDE, 0x71, 0x72 }); // Binary Entitlements magic.
+	uint32_t size = asn1.size() + 8;
+	for (int i = 3; i >= 0; i--) {
+		uint8_t x = (size >> i * 8) & 0xFF; // Size.
+		blob.push_back(x);
+	}
+	blob.append_array(asn1); // Write data.
+}
+
+PackedByteArray CodeSignEntitlementsBinary::get_hash_sha1() const {
+	PackedByteArray hash;
+	hash.resize(0x14);
+
+	CryptoCore::SHA1Context ctx;
+	ctx.start();
+	ctx.update(blob.ptr(), blob.size());
+	ctx.finish(hash.ptrw());
+
+	return hash;
+}
+
+PackedByteArray CodeSignEntitlementsBinary::get_hash_sha256() const {
+	PackedByteArray hash;
+	hash.resize(0x20);
+
+	CryptoCore::SHA256Context ctx;
+	ctx.start();
+	ctx.update(blob.ptr(), blob.size());
+	ctx.finish(hash.ptrw());
+
+	return hash;
+}
+
+int CodeSignEntitlementsBinary::get_size() const {
+	return blob.size();
+}
+
+void CodeSignEntitlementsBinary::write_to_file(FileAccess *p_file) const {
+	ERR_FAIL_COND_MSG(!p_file, "CodeSign/EntitlementsBinary: Invalid file handle.");
+	p_file->store_buffer(blob.ptr(), blob.size());
+}
+
+/*************************************************************************/
+/* CodeSignCodeDirectory                                                 */
+/*************************************************************************/
+
+CodeSignCodeDirectory::CodeSignCodeDirectory() {
+	blob.append_array({ 0xFA, 0xDE, 0x0C, 0x02 }); // Code Directory magic.
+	blob.append_array({ 0x00, 0x00, 0x00, 0x00 }); // Size (8 bytes).
+}
+
+CodeSignCodeDirectory::CodeSignCodeDirectory(uint8_t p_hash_size, uint8_t p_hash_type, bool p_main, const CharString &p_id, const CharString &p_team_id, uint32_t p_page_size, uint64_t p_exe_limit, uint64_t p_code_limit) {
+	pages = p_code_limit / (uint64_t(1) << p_page_size);
+	remain = p_code_limit % (uint64_t(1) << p_page_size);
+	code_slots = pages + (remain > 0 ? 1 : 0);
+	special_slots = 7;
+
+	int cd_size = 8 + sizeof(CodeDirectoryHeader) + (code_slots + special_slots) * p_hash_size + p_id.size() + p_team_id.size();
+	int cd_off = 8 + sizeof(CodeDirectoryHeader);
+	blob.append_array({ 0xFA, 0xDE, 0x0C, 0x02 }); // Code Directory magic.
+	for (int i = 3; i >= 0; i--) {
+		uint8_t x = (cd_size >> i * 8) & 0xFF; // Size.
+		blob.push_back(x);
+	}
+	blob.resize(cd_size);
+	memset(blob.ptrw() + 8, 0x00, cd_size - 8);
+	CodeDirectoryHeader *cd = (CodeDirectoryHeader *)(blob.ptrw() + 8);
+
+	bool is_64_cl = (p_code_limit >= std::numeric_limits<uint32_t>::max());
+
+	// Version and options.
+	cd->version = BSWAP32(0x20500);
+	cd->flags = BSWAP32(SIGNATURE_ADHOC | SIGNATURE_RUNTIME);
+	cd->special_slots = BSWAP32(special_slots);
+	cd->code_slots = BSWAP32(code_slots);
+	if (is_64_cl) {
+		cd->code_limit_64 = BSWAP64(p_code_limit);
+	} else {
+		cd->code_limit = BSWAP32(p_code_limit);
+	}
+	cd->hash_size = p_hash_size;
+	cd->hash_type = p_hash_type;
+	cd->page_size = p_page_size;
+	cd->exec_seg_base = 0x00;
+	cd->exec_seg_limit = BSWAP64(p_exe_limit);
+	cd->exec_seg_flags = 0;
+	if (p_main) {
+		cd->exec_seg_flags |= EXECSEG_MAIN_BINARY;
+	}
+	cd->exec_seg_flags = BSWAP64(cd->exec_seg_flags);
+	uint32_t version = (11 << 16) + (3 << 8) + 0; // Version 11.3.0
+	cd->runtime = BSWAP32(version);
+
+	// Copy ID.
+	cd->ident_offset = BSWAP32(cd_off);
+	memcpy(blob.ptrw() + cd_off, p_id.get_data(), p_id.size());
+	cd_off += p_id.size();
+
+	// Copy Team ID.
+	if (p_team_id.length() > 0) {
+		cd->team_offset = BSWAP32(cd_off);
+		memcpy(blob.ptrw() + cd_off, p_team_id.get_data(), p_team_id.size());
+		cd_off += p_team_id.size();
+	} else {
+		cd->team_offset = 0;
+	}
+
+	// Scatter vector.
+	cd->scatter_vector_offset = 0; // Not used.
+
+	// Executable hashes offset.
+	cd->hash_offset = BSWAP32(cd_off + special_slots * cd->hash_size);
+}
+
+bool CodeSignCodeDirectory::set_hash_in_slot(const PackedByteArray &p_hash, int p_slot) {
+	ERR_FAIL_COND_V_MSG((p_slot < -special_slots) || (p_slot >= code_slots), false, vformat("CodeSign/CodeDirectory: Invalid hash slot index: %d.", p_slot));
+	CodeDirectoryHeader *cd = reinterpret_cast<CodeDirectoryHeader *>(blob.ptrw() + 8);
+	for (int i = 0; i < cd->hash_size; i++) {
+		blob.write[BSWAP32(cd->hash_offset) + p_slot * cd->hash_size + i] = p_hash[i];
+	}
+	return true;
+}
+
+int32_t CodeSignCodeDirectory::get_page_count() {
+	return pages;
+}
+
+int32_t CodeSignCodeDirectory::get_page_remainder() {
+	return remain;
+}
+
+PackedByteArray CodeSignCodeDirectory::get_hash_sha1() const {
+	PackedByteArray hash;
+	hash.resize(0x14);
+
+	CryptoCore::SHA1Context ctx;
+	ctx.start();
+	ctx.update(blob.ptr(), blob.size());
+	ctx.finish(hash.ptrw());
+
+	return hash;
+}
+
+PackedByteArray CodeSignCodeDirectory::get_hash_sha256() const {
+	PackedByteArray hash;
+	hash.resize(0x20);
+
+	CryptoCore::SHA256Context ctx;
+	ctx.start();
+	ctx.update(blob.ptr(), blob.size());
+	ctx.finish(hash.ptrw());
+
+	return hash;
+}
+
+int CodeSignCodeDirectory::get_size() const {
+	return blob.size();
+}
+
+void CodeSignCodeDirectory::write_to_file(FileAccess *p_file) const {
+	ERR_FAIL_COND_MSG(!p_file, "CodeSign/CodeDirectory: Invalid file handle.");
+	p_file->store_buffer(blob.ptr(), blob.size());
+}
+
+/*************************************************************************/
+/* CodeSignSignature                                                     */
+/*************************************************************************/
+
+CodeSignSignature::CodeSignSignature() {
+	blob.append_array({ 0xFA, 0xDE, 0x0B, 0x01 }); // Signature magic.
+	uint32_t sign_size = 8; // Ad-hoc signature is empty.
+	for (int i = 3; i >= 0; i--) {
+		uint8_t x = (sign_size >> i * 8) & 0xFF; // Size.
+		blob.push_back(x);
+	}
+}
+
+PackedByteArray CodeSignSignature::get_hash_sha1() const {
+	PackedByteArray hash;
+	hash.resize(0x14);
+
+	CryptoCore::SHA1Context ctx;
+	ctx.start();
+	ctx.update(blob.ptr(), blob.size());
+	ctx.finish(hash.ptrw());
+
+	return hash;
+}
+
+PackedByteArray CodeSignSignature::get_hash_sha256() const {
+	PackedByteArray hash;
+	hash.resize(0x20);
+
+	CryptoCore::SHA256Context ctx;
+	ctx.start();
+	ctx.update(blob.ptr(), blob.size());
+	ctx.finish(hash.ptrw());
+
+	return hash;
+}
+
+int CodeSignSignature::get_size() const {
+	return blob.size();
+}
+
+void CodeSignSignature::write_to_file(FileAccess *p_file) const {
+	ERR_FAIL_COND_MSG(!p_file, "CodeSign/Signature: Invalid file handle.");
+	p_file->store_buffer(blob.ptr(), blob.size());
+}
+
+/*************************************************************************/
+/* CodeSignSuperBlob                                                     */
+/*************************************************************************/
+
+bool CodeSignSuperBlob::add_blob(const Ref<CodeSignBlob> &p_blob) {
+	if (p_blob.is_valid()) {
+		blobs.push_back(p_blob);
+		return true;
+	} else {
+		return false;
+	}
+}
+
+int CodeSignSuperBlob::get_size() const {
+	int size = 12 + blobs.size() * 8;
+	for (int i = 0; i < blobs.size(); i++) {
+		if (blobs[i].is_null()) {
+			return 0;
+		}
+		size += blobs[i]->get_size();
+	}
+	return size;
+}
+
+void CodeSignSuperBlob::write_to_file(FileAccess *p_file) const {
+	ERR_FAIL_COND_MSG(!p_file, "CodeSign/SuperBlob: Invalid file handle.");
+	uint32_t size = get_size();
+	uint32_t data_offset = 12 + blobs.size() * 8;
+
+	// Write header.
+	p_file->store_32(BSWAP32(0xfade0cc0));
+	p_file->store_32(BSWAP32(size));
+	p_file->store_32(BSWAP32(blobs.size()));
+
+	// Write index.
+	for (int i = 0; i < blobs.size(); i++) {
+		if (blobs[i].is_null()) {
+			return;
+		}
+		p_file->store_32(BSWAP32(blobs[i]->get_index_type()));
+		p_file->store_32(BSWAP32(data_offset));
+		data_offset += blobs[i]->get_size();
+	}
+
+	// Write blobs.
+	for (int i = 0; i < blobs.size(); i++) {
+		blobs[i]->write_to_file(p_file);
+	}
+}
+
+/*************************************************************************/
+/* CodeSign                                                              */
+/*************************************************************************/
+
+PackedByteArray CodeSign::file_hash_sha1(const String &p_path) {
+	PackedByteArray file_hash;
+	FileAccessRef f = FileAccess::open(p_path, FileAccess::READ);
+	ERR_FAIL_COND_V_MSG(!f, PackedByteArray(), vformat("CodeSign: Can't open file: \"%s\".", p_path));
+
+	CryptoCore::SHA1Context ctx;
+	ctx.start();
+
+	unsigned char step[4096];
+	while (true) {
+		uint64_t br = f->get_buffer(step, 4096);
+		if (br > 0) {
+			ctx.update(step, br);
+		}
+		if (br < 4096) {
+			break;
+		}
+	}
+
+	file_hash.resize(0x14);
+	ctx.finish(file_hash.ptrw());
+	return file_hash;
+}
+
+PackedByteArray CodeSign::file_hash_sha256(const String &p_path) {
+	PackedByteArray file_hash;
+	FileAccessRef f = FileAccess::open(p_path, FileAccess::READ);
+	ERR_FAIL_COND_V_MSG(!f, PackedByteArray(), vformat("CodeSign: Can't open file: \"%s\".", p_path));
+
+	CryptoCore::SHA256Context ctx;
+	ctx.start();
+
+	unsigned char step[4096];
+	while (true) {
+		uint64_t br = f->get_buffer(step, 4096);
+		if (br > 0) {
+			ctx.update(step, br);
+		}
+		if (br < 4096) {
+			break;
+		}
+	}
+
+	file_hash.resize(0x20);
+	ctx.finish(file_hash.ptrw());
+	return file_hash;
+}
+
+Error CodeSign::_codesign_file(bool p_use_hardened_runtime, bool p_force, const String &p_info, const String &p_exe_path, const String &p_bundle_path, const String &p_ent_path, bool p_ios_bundle, String &r_error_msg) {
+#define CLEANUP()                                        \
+	if (files_to_sign.size() > 1) {                      \
+		for (int j = 0; j < files_to_sign.size(); j++) { \
+			da->remove(files_to_sign[j]);                \
+		}                                                \
+	}
+
+	print_verbose(vformat("CodeSign: Signing executable: %s, bundle: %s with entitlements %s", p_exe_path, p_bundle_path, p_ent_path));
+
+	PackedByteArray info_hash1, info_hash2;
+	PackedByteArray res_hash1, res_hash2;
+	String id;
+	String main_exe = p_exe_path;
+
+	DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+	if (!da) {
+		r_error_msg = TTR("Can't get filesystem access.");
+		ERR_FAIL_V_MSG(ERR_CANT_CREATE, "CodeSign: Can't get filesystem access.");
+	}
+
+	// Read Info.plist.
+	if (!p_info.is_empty()) {
+		print_verbose(vformat("CodeSign: Reading bundle info..."));
+		PList info_plist;
+		if (info_plist.load_file(p_info)) {
+			info_hash1 = file_hash_sha1(p_info);
+			info_hash2 = file_hash_sha256(p_info);
+			if (info_hash1.is_empty() || info_hash2.is_empty()) {
+				r_error_msg = TTR("Failed to get Info.plist hash.");
+				ERR_FAIL_V_MSG(FAILED, "CodeSign: Failed to get Info.plist hash.");
+			}
+
+			if (info_plist.get_root()->data_type == PList::PLNodeType::PL_NODE_TYPE_DICT && info_plist.get_root()->data_dict.has("CFBundleExecutable")) {
+				main_exe = p_exe_path.plus_file(String::utf8(info_plist.get_root()->data_dict["CFBundleExecutable"]->data_string.get_data()));
+			} else {
+				r_error_msg = TTR("Invalid Info.plist, no exe name.");
+				ERR_FAIL_V_MSG(FAILED, "CodeSign: Invalid Info.plist, no exe name.");
+			}
+
+			if (info_plist.get_root()->data_type == PList::PLNodeType::PL_NODE_TYPE_DICT && info_plist.get_root()->data_dict.has("CFBundleIdentifier")) {
+				id = info_plist.get_root()->data_dict["CFBundleIdentifier"]->data_string.get_data();
+			} else {
+				r_error_msg = TTR("Invalid Info.plist, no bundle id.");
+				ERR_FAIL_V_MSG(FAILED, "CodeSign: Invalid Info.plist, no bundle id.");
+			}
+		} else {
+			r_error_msg = TTR("Invalid Info.plist, can't load.");
+			ERR_FAIL_V_MSG(FAILED, "CodeSign: Invalid Info.plist, can't load.");
+		}
+	}
+
+	// Extract fat binary.
+	Vector<String> files_to_sign;
+	if (LipO::is_lipo(main_exe)) {
+		print_verbose(vformat("CodeSign: Executable is fat, extracting..."));
+		String tmp_path_name = EditorPaths::get_singleton()->get_cache_dir().plus_file("_lipo");
+		Error err = da->make_dir_recursive(tmp_path_name);
+		if (err != OK) {
+			r_error_msg = vformat(TTR("Failed to create \"%s\" subfolder."), tmp_path_name);
+			ERR_FAIL_V_MSG(FAILED, vformat("CodeSign: Failed to create \"%s\" subfolder.", tmp_path_name));
+		}
+		LipO lip;
+		if (lip.open_file(main_exe)) {
+			for (int i = 0; i < lip.get_arch_count(); i++) {
+				if (!lip.extract_arch(i, tmp_path_name.plus_file("_exe_" + itos(i)))) {
+					CLEANUP();
+					r_error_msg = TTR("Failed to extract thin binary.");
+					ERR_FAIL_V_MSG(FAILED, "CodeSign: Failed to extract thin binary.");
+				}
+				files_to_sign.push_back(tmp_path_name.plus_file("_exe_" + itos(i)));
+			}
+		}
+	} else if (MachO::is_macho(main_exe)) {
+		print_verbose(vformat("CodeSign: Executable is thin..."));
+		files_to_sign.push_back(main_exe);
+	} else {
+		r_error_msg = TTR("Invalid binary format.");
+		ERR_FAIL_V_MSG(FAILED, "CodeSign: Invalid binary format.");
+	}
+
+	// Check if it's already signed.
+	if (!p_force) {
+		for (int i = 0; i < files_to_sign.size(); i++) {
+			MachO mh;
+			mh.open_file(files_to_sign[i]);
+			if (mh.is_signed()) {
+				CLEANUP();
+				r_error_msg = TTR("Already signed!");
+				ERR_FAIL_V_MSG(FAILED, "CodeSign: Already signed!");
+			}
+		}
+	}
+
+	// Generate core resources.
+	if (!p_bundle_path.is_empty()) {
+		print_verbose(vformat("CodeSign: Generating bundle CodeResources..."));
+		CodeSignCodeResources cr;
+
+		if (p_ios_bundle) {
+			cr.add_rule1("^.*");
+			cr.add_rule1("^.*\\.lproj/", "optional", 100);
+			cr.add_rule1("^.*\\.lproj/locversion.plist$", "omit", 1100);
+			cr.add_rule1("^Base\\.lproj/", "", 1010);
+			cr.add_rule1("^version.plist$");
+
+			cr.add_rule2(".*\\.dSYM($|/)", "", 11);
+			cr.add_rule2("^(.*/)?\\.DS_Store$", "omit", 2000);
+			cr.add_rule2("^.*");
+			cr.add_rule2("^.*\\.lproj/", "optional", 1000);
+			cr.add_rule2("^.*\\.lproj/locversion.plist$", "omit", 1100);
+			cr.add_rule2("^Base\\.lproj/", "", 1010);
+			cr.add_rule2("^Info\\.plist$", "omit", 20);
+			cr.add_rule2("^PkgInfo$", "omit", 20);
+			cr.add_rule2("^embedded\\.provisionprofile$", "", 10);
+			cr.add_rule2("^version\\.plist$", "", 20);
+
+			cr.add_rule2("^_MASReceipt", "omit", 2000, false);
+			cr.add_rule2("^_CodeSignature", "omit", 2000, false);
+			cr.add_rule2("^CodeResources", "omit", 2000, false);
+		} else {
+			cr.add_rule1("^Resources/");
+			cr.add_rule1("^Resources/.*\\.lproj/", "optional", 1000);
+			cr.add_rule1("^Resources/.*\\.lproj/locversion.plist$", "omit", 1100);
+			cr.add_rule1("^Resources/Base\\.lproj/", "", 1010);
+			cr.add_rule1("^version.plist$");
+
+			cr.add_rule2(".*\\.dSYM($|/)", "", 11);
+			cr.add_rule2("^(.*/)?\\.DS_Store$", "omit", 2000);
+			cr.add_rule2("^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/", "nested", 10);
+			cr.add_rule2("^.*");
+			cr.add_rule2("^Info\\.plist$", "omit", 20);
+			cr.add_rule2("^PkgInfo$", "omit", 20);
+			cr.add_rule2("^Resources/", "", 20);
+			cr.add_rule2("^Resources/.*\\.lproj/", "optional", 1000);
+			cr.add_rule2("^Resources/.*\\.lproj/locversion.plist$", "omit", 1100);
+			cr.add_rule2("^Resources/Base\\.lproj/", "", 1010);
+			cr.add_rule2("^[^/]+$", "nested", 10);
+			cr.add_rule2("^embedded\\.provisionprofile$", "", 10);
+			cr.add_rule2("^version\\.plist$", "", 20);
+			cr.add_rule2("^_MASReceipt", "omit", 2000, false);
+			cr.add_rule2("^_CodeSignature", "omit", 2000, false);
+			cr.add_rule2("^CodeResources", "omit", 2000, false);
+		}
+
+		if (!cr.add_folder_recursive(p_bundle_path, "", main_exe)) {
+			CLEANUP();
+			r_error_msg = TTR("Failed to process nested resources.");
+			ERR_FAIL_V_MSG(FAILED, "CodeSign: Failed to process nested resources.");
+		}
+		Error err = da->make_dir_recursive(p_bundle_path.plus_file("_CodeSignature"));
+		if (err != OK) {
+			CLEANUP();
+			r_error_msg = TTR("Failed to create _CodeSignature subfolder.");
+			ERR_FAIL_V_MSG(FAILED, "CodeSign: Failed to create _CodeSignature subfolder.");
+		}
+		cr.save_to_file(p_bundle_path.plus_file("_CodeSignature").plus_file("CodeResources"));
+		res_hash1 = file_hash_sha1(p_bundle_path.plus_file("_CodeSignature").plus_file("CodeResources"));
+		res_hash2 = file_hash_sha256(p_bundle_path.plus_file("_CodeSignature").plus_file("CodeResources"));
+		if (res_hash1.is_empty() || res_hash2.is_empty()) {
+			CLEANUP();
+			r_error_msg = TTR("Failed to get CodeResources hash.");
+			ERR_FAIL_V_MSG(FAILED, "CodeSign: Failed to get CodeResources hash.");
+		}
+	}
+
+	// Generate common signature structures.
+	if (id.is_empty()) {
+		Ref<Crypto> crypto = Ref<Crypto>(Crypto::create());
+		PackedByteArray uuid = crypto->generate_random_bytes(16);
+		id = (String("a-55554944") /*a-UUID*/ + String::hex_encode_buffer(uuid.ptr(), 16));
+	}
+	CharString uuid_str = id.utf8();
+	print_verbose(vformat("CodeSign: Used bundle ID: %s", id));
+
+	print_verbose(vformat("CodeSign: Processing entitlements..."));
+
+	Ref<CodeSignEntitlementsText> cet;
+	Ref<CodeSignEntitlementsBinary> ceb;
+	if (!p_ent_path.is_empty()) {
+		String entitlements = FileAccess::get_file_as_string(p_ent_path);
+		if (entitlements.is_empty()) {
+			CLEANUP();
+			r_error_msg = TTR("Invalid entitlements file.");
+			ERR_FAIL_V_MSG(FAILED, "CodeSign: Invalid entitlements file.");
+		}
+		cet = Ref<CodeSignEntitlementsText>(memnew(CodeSignEntitlementsText(entitlements)));
+		ceb = Ref<CodeSignEntitlementsBinary>(memnew(CodeSignEntitlementsBinary(entitlements)));
+	}
+
+	print_verbose(vformat("CodeSign: Generating requirements..."));
+	Ref<CodeSignRequirements> rq;
+	String team_id = "";
+	rq = Ref<CodeSignRequirements>(memnew(CodeSignRequirements()));
+
+	// Sign executables.
+	for (int i = 0; i < files_to_sign.size(); i++) {
+		MachO mh;
+		if (!mh.open_file(files_to_sign[i])) {
+			CLEANUP();
+			r_error_msg = TTR("Invalid executable file.");
+			ERR_FAIL_V_MSG(FAILED, "CodeSign: Invalid executable file.");
+		}
+		print_verbose(vformat("CodeSign: Signing executable for cputype: %d ...", mh.get_cputype()));
+
+		print_verbose(vformat("CodeSign: Generating CodeDirectory..."));
+		Ref<CodeSignCodeDirectory> cd1 = memnew(CodeSignCodeDirectory(0x14, 0x01, true, uuid_str, team_id.utf8(), 12, mh.get_exe_limit(), mh.get_code_limit()));
+		Ref<CodeSignCodeDirectory> cd2 = memnew(CodeSignCodeDirectory(0x20, 0x02, true, uuid_str, team_id.utf8(), 12, mh.get_exe_limit(), mh.get_code_limit()));
+		print_verbose(vformat("CodeSign: Calculating special slot hashes..."));
+		if (info_hash2.size() == 0x20) {
+			cd2->set_hash_in_slot(info_hash2, CodeSignCodeDirectory::SLOT_INFO_PLIST);
+		}
+		if (info_hash1.size() == 0x14) {
+			cd1->set_hash_in_slot(info_hash1, CodeSignCodeDirectory::SLOT_INFO_PLIST);
+		}
+		cd1->set_hash_in_slot(rq->get_hash_sha1(), CodeSignCodeDirectory::Slot::SLOT_REQUIREMENTS);
+		cd2->set_hash_in_slot(rq->get_hash_sha256(), CodeSignCodeDirectory::Slot::SLOT_REQUIREMENTS);
+		if (res_hash2.size() == 0x20) {
+			cd2->set_hash_in_slot(res_hash2, CodeSignCodeDirectory::SLOT_RESOURCES);
+		}
+		if (res_hash1.size() == 0x14) {
+			cd1->set_hash_in_slot(res_hash1, CodeSignCodeDirectory::SLOT_RESOURCES);
+		}
+		if (cet.is_valid()) {
+			cd1->set_hash_in_slot(cet->get_hash_sha1(), CodeSignCodeDirectory::Slot::SLOT_ENTITLEMENTS); //Text variant.
+			cd2->set_hash_in_slot(cet->get_hash_sha256(), CodeSignCodeDirectory::Slot::SLOT_ENTITLEMENTS);
+		}
+		if (ceb.is_valid()) {
+			cd1->set_hash_in_slot(ceb->get_hash_sha1(), CodeSignCodeDirectory::Slot::SLOT_DER_ENTITLEMENTS); //ASN.1 variant.
+			cd2->set_hash_in_slot(ceb->get_hash_sha256(), CodeSignCodeDirectory::Slot::SLOT_DER_ENTITLEMENTS);
+		}
+
+		// Calculate signature size.
+		int sign_size = 12; // SuperBlob header.
+		sign_size += cd1->get_size() + 8;
+		sign_size += cd2->get_size() + 8;
+		sign_size += rq->get_size() + 8;
+		if (cet.is_valid()) {
+			sign_size += cet->get_size() + 8;
+		}
+		if (ceb.is_valid()) {
+			sign_size += ceb->get_size() + 8;
+		}
+		sign_size += 16; // Empty signature size.
+
+		// Alloc/resize signature load command.
+		print_verbose(vformat("CodeSign: Reallocating space for the signature superblob (%d)...", sign_size));
+		if (!mh.set_signature_size(sign_size)) {
+			CLEANUP();
+			r_error_msg = TTR("Can't resize signature load command.");
+			ERR_FAIL_V_MSG(FAILED, "CodeSign: Can't resize signature load command.");
+		}
+
+		print_verbose(vformat("CodeSign: Calculating executable code hashes..."));
+		// Calculate executable code hashes.
+		PackedByteArray buffer;
+		PackedByteArray hash1, hash2;
+		hash1.resize(0x14);
+		hash2.resize(0x20);
+		buffer.resize(1 << 12);
+		mh.get_file()->seek(0);
+		for (int32_t j = 0; j < cd2->get_page_count(); j++) {
+			mh.get_file()->get_buffer(buffer.ptrw(), (1 << 12));
+			CryptoCore::SHA256Context ctx2;
+			ctx2.start();
+			ctx2.update(buffer.ptr(), (1 << 12));
+			ctx2.finish(hash2.ptrw());
+			cd2->set_hash_in_slot(hash2, j);
+
+			CryptoCore::SHA1Context ctx1;
+			ctx1.start();
+			ctx1.update(buffer.ptr(), (1 << 12));
+			ctx1.finish(hash1.ptrw());
+			cd1->set_hash_in_slot(hash1, j);
+		}
+		if (cd2->get_page_remainder() > 0) {
+			mh.get_file()->get_buffer(buffer.ptrw(), cd2->get_page_remainder());
+			CryptoCore::SHA256Context ctx2;
+			ctx2.start();
+			ctx2.update(buffer.ptr(), cd2->get_page_remainder());
+			ctx2.finish(hash2.ptrw());
+			cd2->set_hash_in_slot(hash2, cd2->get_page_count());
+
+			CryptoCore::SHA1Context ctx1;
+			ctx1.start();
+			ctx1.update(buffer.ptr(), cd1->get_page_remainder());
+			ctx1.finish(hash1.ptrw());
+			cd1->set_hash_in_slot(hash1, cd1->get_page_count());
+		}
+
+		print_verbose(vformat("CodeSign: Generating signature..."));
+		Ref<CodeSignSignature> cs;
+		cs = Ref<CodeSignSignature>(memnew(CodeSignSignature()));
+
+		print_verbose(vformat("CodeSign: Writing signature superblob..."));
+		// Write signature data to the executable.
+		CodeSignSuperBlob sb = CodeSignSuperBlob();
+		sb.add_blob(cd2);
+		sb.add_blob(cd1);
+		sb.add_blob(rq);
+		if (cet.is_valid()) {
+			sb.add_blob(cet);
+		}
+		if (ceb.is_valid()) {
+			sb.add_blob(ceb);
+		}
+		sb.add_blob(cs);
+		mh.get_file()->seek(mh.get_signature_offset());
+		sb.write_to_file(mh.get_file());
+	}
+	if (files_to_sign.size() > 1) {
+		print_verbose(vformat("CodeSign: Rebuilding fat executable..."));
+		LipO lip;
+		if (!lip.create_file(main_exe, files_to_sign)) {
+			CLEANUP();
+			r_error_msg = TTR("Failed to create fat binary.");
+			ERR_FAIL_V_MSG(FAILED, "CodeSign: Failed to create fat binary.");
+		}
+		CLEANUP();
+	}
+	FileAccess::set_unix_permissions(main_exe, 0755); // Restore unix permissions.
+	return OK;
+#undef CLEANUP
+}
+
+Error CodeSign::codesign(bool p_use_hardened_runtime, bool p_force, const String &p_path, const String &p_ent_path, String &r_error_msg) {
+	DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+	if (!da) {
+		r_error_msg = TTR("Can't get filesystem access.");
+		ERR_FAIL_V_MSG(ERR_CANT_CREATE, "CodeSign: Can't get filesystem access.");
+	}
+
+	if (da->dir_exists(p_path)) {
+		String fmw_ver = "Current"; // Framework version (default).
+		String info_path;
+		String main_exe;
+		String bundle_path;
+		bool bundle = false;
+		bool ios_bundle = false;
+		if (da->file_exists(p_path.plus_file("Contents/Info.plist"))) {
+			info_path = p_path.plus_file("Contents/Info.plist");
+			main_exe = p_path.plus_file("Contents/MacOS");
+			bundle_path = p_path.plus_file("Contents");
+			bundle = true;
+		} else if (da->file_exists(p_path.plus_file(vformat("Versions/%s/Resources/Info.plist", fmw_ver)))) {
+			info_path = p_path.plus_file(vformat("Versions/%s/Resources/Info.plist", fmw_ver));
+			main_exe = p_path.plus_file(vformat("Versions/%s", fmw_ver));
+			bundle_path = p_path.plus_file(vformat("Versions/%s", fmw_ver));
+			bundle = true;
+		} else if (da->file_exists(p_path.plus_file("Info.plist"))) {
+			info_path = p_path.plus_file("Info.plist");
+			main_exe = p_path;
+			bundle_path = p_path;
+			bundle = true;
+			ios_bundle = true;
+		}
+		if (bundle) {
+			return _codesign_file(p_use_hardened_runtime, p_force, info_path, main_exe, bundle_path, p_ent_path, ios_bundle, r_error_msg);
+		} else {
+			r_error_msg = TTR("Unknown bundle type.");
+			ERR_FAIL_V_MSG(FAILED, "CodeSign: Unknown bundle type.");
+		}
+	} else if (da->file_exists(p_path)) {
+		return _codesign_file(p_use_hardened_runtime, p_force, "", p_path, "", p_ent_path, false, r_error_msg);
+	} else {
+		r_error_msg = TTR("Unknown object type.");
+		ERR_FAIL_V_MSG(FAILED, "CodeSign: Unknown object type.");
+	}
+}
+
+#endif // MODULE_REGEX_ENABLED

+ 369 - 0
platform/osx/export/codesign.h

@@ -0,0 +1,369 @@
+/*************************************************************************/
+/*  codesign.h                                                           */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+// macOS code signature creation utility.
+//
+// Current implementation has the following limitation:
+//  - Only version 11.3.0 signatures are supported.
+//  - Only "framework" and "app" bundle types are supported.
+//  - Page hash array scattering is not supported.
+//  - Reading and writing binary property lists i snot supported (third-party frameworks with binary Info.plist will not work unless .plist is converted to text format).
+//  - Requirements code generator is not implemented (only hard-coded requirements for the ad-hoc signing is supported).
+//  - RFC5652/CMS blob generation is not implemented, supports ad-hoc signing only.
+
+#ifndef CODESIGN_H
+#define CODESIGN_H
+
+#include "core/crypto/crypto.h"
+#include "core/crypto/crypto_core.h"
+#include "core/io/dir_access.h"
+#include "core/io/file_access.h"
+#include "core/object/ref_counted.h"
+
+#include "modules/modules_enabled.gen.h" // For regex.
+#ifdef MODULE_REGEX_ENABLED
+#include "modules/regex/regex.h"
+#endif
+
+#include "plist.h"
+
+#ifdef MODULE_REGEX_ENABLED
+
+/*************************************************************************/
+/* CodeSignCodeResources                                                 */
+/*************************************************************************/
+
+class CodeSignCodeResources {
+public:
+	enum class CRMatch {
+		CR_MATCH_NO,
+		CR_MATCH_YES,
+		CR_MATCH_NESTED,
+		CR_MATCH_OPTIONAL,
+	};
+
+private:
+	struct CRFile {
+		String name;
+		String hash;
+		String hash2;
+		bool optional;
+		bool nested;
+		String requirements;
+	};
+
+	struct CRRule {
+		String file_pattern;
+		String key;
+		int weight;
+		bool store;
+		CRRule() {
+			weight = 1;
+			store = true;
+		}
+		CRRule(const String &p_file_pattern, const String &p_key, int p_weight, bool p_store) {
+			file_pattern = p_file_pattern;
+			key = p_key;
+			weight = p_weight;
+			store = p_store;
+		}
+	};
+
+	Vector<CRRule> rules1;
+	Vector<CRRule> rules2;
+
+	Vector<CRFile> files1;
+	Vector<CRFile> files2;
+
+	String hash_sha1_base64(const String &p_path);
+	String hash_sha256_base64(const String &p_path);
+
+public:
+	void add_rule1(const String &p_rule, const String &p_key = "", int p_weight = 0, bool p_store = true);
+	void add_rule2(const String &p_rule, const String &p_key = "", int p_weight = 0, bool p_store = true);
+
+	CRMatch match_rules1(const String &p_path) const;
+	CRMatch match_rules2(const String &p_path) const;
+
+	bool add_file1(const String &p_root, const String &p_path);
+	bool add_file2(const String &p_root, const String &p_path);
+	bool add_nested_file(const String &p_root, const String &p_path, const String &p_exepath);
+
+	bool add_folder_recursive(const String &p_root, const String &p_path = "", const String &p_main_exe_path = "");
+
+	bool save_to_file(const String &p_path);
+};
+
+/*************************************************************************/
+/* CodeSignBlob                                                          */
+/*************************************************************************/
+
+class CodeSignBlob : public RefCounted {
+public:
+	virtual PackedByteArray get_hash_sha1() const = 0;
+	virtual PackedByteArray get_hash_sha256() const = 0;
+
+	virtual int get_size() const = 0;
+	virtual uint32_t get_index_type() const = 0;
+
+	virtual void write_to_file(FileAccess *p_file) const = 0;
+};
+
+/*************************************************************************/
+/* CodeSignRequirements                                                  */
+/*************************************************************************/
+
+// Note: Proper code generator is not implemented (any we probably won't ever need it), just a hardcoded bytecode for the limited set of cases.
+
+class CodeSignRequirements : public CodeSignBlob {
+	PackedByteArray blob;
+
+	static inline size_t PAD(size_t s, size_t a) {
+		return (s % a == 0) ? 0 : (a - s % a);
+	}
+
+	_FORCE_INLINE_ void _parse_certificate_slot(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const;
+	_FORCE_INLINE_ void _parse_key(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const;
+	_FORCE_INLINE_ void _parse_oid_key(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const;
+	_FORCE_INLINE_ void _parse_hash_string(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const;
+	_FORCE_INLINE_ void _parse_value(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const;
+	_FORCE_INLINE_ void _parse_date(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const;
+	_FORCE_INLINE_ bool _parse_match(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const;
+
+public:
+	CodeSignRequirements();
+	CodeSignRequirements(const PackedByteArray &p_data);
+
+	Vector<String> parse_requirements() const;
+
+	virtual PackedByteArray get_hash_sha1() const override;
+	virtual PackedByteArray get_hash_sha256() const override;
+
+	virtual int get_size() const override;
+
+	virtual uint32_t get_index_type() const override { return 0x00000002; };
+	virtual void write_to_file(FileAccess *p_file) const override;
+};
+
+/*************************************************************************/
+/* CodeSignEntitlementsText                                             */
+/*************************************************************************/
+
+// PList formatted entitlements.
+
+class CodeSignEntitlementsText : public CodeSignBlob {
+	PackedByteArray blob;
+
+public:
+	CodeSignEntitlementsText();
+	CodeSignEntitlementsText(const String &p_string);
+
+	virtual PackedByteArray get_hash_sha1() const override;
+	virtual PackedByteArray get_hash_sha256() const override;
+
+	virtual int get_size() const override;
+
+	virtual uint32_t get_index_type() const override { return 0x00000005; };
+	virtual void write_to_file(FileAccess *p_file) const override;
+};
+
+/*************************************************************************/
+/* CodeSignEntitlementsBinary                                           */
+/*************************************************************************/
+
+// ASN.1 serialized entitlements.
+
+class CodeSignEntitlementsBinary : public CodeSignBlob {
+	PackedByteArray blob;
+
+public:
+	CodeSignEntitlementsBinary();
+	CodeSignEntitlementsBinary(const String &p_string);
+
+	virtual PackedByteArray get_hash_sha1() const override;
+	virtual PackedByteArray get_hash_sha256() const override;
+
+	virtual int get_size() const override;
+
+	virtual uint32_t get_index_type() const override { return 0x00000007; };
+	virtual void write_to_file(FileAccess *p_file) const override;
+};
+
+/*************************************************************************/
+/* CodeSignCodeDirectory                                                 */
+/*************************************************************************/
+
+// Code Directory, runtime options, code segment and special structure hashes.
+
+class CodeSignCodeDirectory : public CodeSignBlob {
+public:
+	enum Slot {
+		SLOT_INFO_PLIST = -1,
+		SLOT_REQUIREMENTS = -2,
+		SLOT_RESOURCES = -3,
+		SLOT_APP_SPECIFIC = -4, // Unused.
+		SLOT_ENTITLEMENTS = -5,
+		SLOT_RESERVER1 = -6, // Unused.
+		SLOT_DER_ENTITLEMENTS = -7,
+	};
+
+	enum CodeSignExecSegFlags {
+		EXECSEG_MAIN_BINARY = 0x1,
+		EXECSEG_ALLOW_UNSIGNED = 0x10,
+		EXECSEG_DEBUGGER = 0x20,
+		EXECSEG_JIT = 0x40,
+		EXECSEG_SKIP_LV = 0x80,
+		EXECSEG_CAN_LOAD_CDHASH = 0x100,
+		EXECSEG_CAN_EXEC_CDHASH = 0x200,
+	};
+
+	enum CodeSignatureFlags {
+		SIGNATURE_HOST = 0x0001,
+		SIGNATURE_ADHOC = 0x0002,
+		SIGNATURE_TASK_ALLOW = 0x0004,
+		SIGNATURE_INSTALLER = 0x0008,
+		SIGNATURE_FORCED_LV = 0x0010,
+		SIGNATURE_INVALID_ALLOWED = 0x0020,
+		SIGNATURE_FORCE_HARD = 0x0100,
+		SIGNATURE_FORCE_KILL = 0x0200,
+		SIGNATURE_FORCE_EXPIRATION = 0x0400,
+		SIGNATURE_RESTRICT = 0x0800,
+		SIGNATURE_ENFORCEMENT = 0x1000,
+		SIGNATURE_LIBRARY_VALIDATION = 0x2000,
+		SIGNATURE_ENTITLEMENTS_VALIDATED = 0x4000,
+		SIGNATURE_NVRAM_UNRESTRICTED = 0x8000,
+		SIGNATURE_RUNTIME = 0x10000,
+		SIGNATURE_LINKER_SIGNED = 0x20000,
+	};
+
+private:
+	PackedByteArray blob;
+
+	struct CodeDirectoryHeader {
+		uint32_t version; // Using version 0x0020500.
+		uint32_t flags; // // Option flags.
+		uint32_t hash_offset; // Slot zero offset.
+		uint32_t ident_offset; // Identifier string offset.
+		uint32_t special_slots; // Nr. of slots with negative index.
+		uint32_t code_slots; // Nr. of slots with index >= 0, (code_limit / page_size).
+		uint32_t code_limit; // Everything before code signature load command offset.
+		uint8_t hash_size; // 20 (SHA-1) or 32 (SHA-256).
+		uint8_t hash_type; // 1 (SHA-1) or 2 (SHA-256).
+		uint8_t platform; // Not used.
+		uint8_t page_size; // Page size, power of two, 2^12 (4096).
+		uint32_t spare2; // Not used.
+		// Version 0x20100
+		uint32_t scatter_vector_offset; // Set to 0 and ignore.
+		// Version 0x20200
+		uint32_t team_offset; // Team id string offset.
+		// Version 0x20300
+		uint32_t spare3; // Not used.
+		uint64_t code_limit_64; // Set to 0 and ignore.
+		// Version 0x20400
+		uint64_t exec_seg_base; // Start of the signed code segmet.
+		uint64_t exec_seg_limit; // Code segment (__TEXT) vmsize.
+		uint64_t exec_seg_flags; // Executable segment flags.
+		// Version 0x20500
+		uint32_t runtime; // Runtime version.
+		uint32_t pre_encrypt_offset; // Set to 0 and ignore.
+	};
+
+	int32_t pages = 0;
+	int32_t remain = 0;
+	int32_t code_slots = 0;
+	int32_t special_slots = 0;
+
+public:
+	CodeSignCodeDirectory();
+	CodeSignCodeDirectory(uint8_t p_hash_size, uint8_t p_hash_type, bool p_main, const CharString &p_id, const CharString &p_team_id, uint32_t p_page_size, uint64_t p_exe_limit, uint64_t p_code_limit);
+
+	int32_t get_page_count();
+	int32_t get_page_remainder();
+
+	bool set_hash_in_slot(const PackedByteArray &p_hash, int p_slot);
+
+	virtual PackedByteArray get_hash_sha1() const override;
+	virtual PackedByteArray get_hash_sha256() const override;
+
+	virtual int get_size() const override;
+	virtual uint32_t get_index_type() const override { return 0x00000000; };
+
+	virtual void write_to_file(FileAccess *p_file) const override;
+};
+
+/*************************************************************************/
+/* CodeSignSignature                                                     */
+/*************************************************************************/
+
+class CodeSignSignature : public CodeSignBlob {
+	PackedByteArray blob;
+
+public:
+	CodeSignSignature();
+
+	virtual PackedByteArray get_hash_sha1() const override;
+	virtual PackedByteArray get_hash_sha256() const override;
+
+	virtual int get_size() const override;
+	virtual uint32_t get_index_type() const override { return 0x00010000; };
+
+	virtual void write_to_file(FileAccess *p_file) const override;
+};
+
+/*************************************************************************/
+/* CodeSignSuperBlob                                                     */
+/*************************************************************************/
+
+class CodeSignSuperBlob {
+	Vector<Ref<CodeSignBlob>> blobs;
+
+public:
+	bool add_blob(const Ref<CodeSignBlob> &p_blob);
+
+	int get_size() const;
+	void write_to_file(FileAccess *p_file) const;
+};
+
+/*************************************************************************/
+/* CodeSign                                                              */
+/*************************************************************************/
+
+class CodeSign {
+	static PackedByteArray file_hash_sha1(const String &p_path);
+	static PackedByteArray file_hash_sha256(const String &p_path);
+	static Error _codesign_file(bool p_use_hardened_runtime, bool p_force, const String &p_info, const String &p_exe_path, const String &p_bundle_path, const String &p_ent_path, bool p_ios_bundle, String &r_error_msg);
+
+public:
+	static Error codesign(bool p_use_hardened_runtime, bool p_force, const String &p_path, const String &p_ent_path, String &r_error_msg);
+};
+
+#endif // MODULE_REGEX_ENABLED
+
+#endif // CODESIGN_H

+ 3 - 0
platform/osx/export/export.cpp

@@ -33,6 +33,9 @@
 #include "export_plugin.h"
 
 void register_osx_exporter() {
+	EDITOR_DEF("export/macos/force_builtin_codesign", false);
+	EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::BOOL, "export/macos/force_builtin_codesign", PROPERTY_HINT_NONE));
+
 	Ref<EditorExportPlatformOSX> platform;
 	platform.instantiate();
 

+ 279 - 98
platform/osx/export/export_plugin.cpp

@@ -28,6 +28,9 @@
 /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
 /*************************************************************************/
 
+#include "modules/modules_enabled.gen.h" // For regex.
+
+#include "codesign.h"
 #include "export_plugin.h"
 
 void EditorExportPlatformOSX::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) {
@@ -58,15 +61,25 @@ void EditorExportPlatformOSX::get_export_options(List<ExportOption> *r_options)
 	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/version"), "1.0"));
 	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/copyright"), ""));
 	r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "display/high_res"), false));
-	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/camera_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the camera"), ""));
 	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/microphone_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the microphone"), ""));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/camera_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the camera"), ""));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/location_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the location information"), ""));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/address_book_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the address book"), ""));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/calendar_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the calendar"), ""));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/photos_library_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the photo library"), ""));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/desktop_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Desktop folder"), ""));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/documents_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Documents folder"), ""));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/downloads_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Downloads folder"), ""));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/network_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use network volumes"), ""));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/removable_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use removable volumes"), ""));
 
-#ifdef OSX_ENABLED
 	r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/enable"), true));
+#ifdef OSX_ENABLED
 	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/identity", PROPERTY_HINT_PLACEHOLDER_TEXT, "Type: Name (ID)"), ""));
 	r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/timestamp"), true));
-	r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/hardened_runtime"), true));
+#endif
 	r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/replace_existing_signature"), true));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/hardened_runtime"), true));
 	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/entitlements/custom_file", PROPERTY_HINT_GLOBAL_FILE, "*.plist"), ""));
 
 	if (!Engine::get_singleton()->has_singleton("GodotSharp")) {
@@ -97,6 +110,7 @@ void EditorExportPlatformOSX::get_export_options(List<ExportOption> *r_options)
 	r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_movies", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));
 	r_options->push_back(ExportOption(PropertyInfo(Variant::ARRAY, "codesign/entitlements/app_sandbox/helper_executables", PROPERTY_HINT_ARRAY_TYPE, itos(Variant::STRING) + "/" + itos(PROPERTY_HINT_GLOBAL_FILE) + ":"), Array()));
 
+#ifdef OSX_ENABLED
 	r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "codesign/custom_options"), PackedStringArray()));
 
 	r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "notarization/enable"), false));
@@ -305,13 +319,56 @@ void EditorExportPlatformOSX::_fix_plist(const Ref<EditorExportPreset> &p_preset
 		} else if (lines[i].find("$copyright") != -1) {
 			strnew += lines[i].replace("$copyright", p_preset->get("application/copyright")) + "\n";
 		} else if (lines[i].find("$highres") != -1) {
-			strnew += lines[i].replace("$highres", p_preset->get("display/high_res") ? "<true/>" : "<false/>") + "\n";
-		} else if (lines[i].find("$camera_usage_description") != -1) {
-			String description = p_preset->get("privacy/camera_usage_description");
-			strnew += lines[i].replace("$camera_usage_description", description) + "\n";
-		} else if (lines[i].find("$microphone_usage_description") != -1) {
-			String description = p_preset->get("privacy/microphone_usage_description");
-			strnew += lines[i].replace("$microphone_usage_description", description) + "\n";
+			strnew += lines[i].replace("$highres", p_preset->get("display/high_res") ? "\t<true/>" : "\t<false/>") + "\n";
+		} else if (lines[i].find("$usage_descriptions") != -1) {
+			String descriptions;
+			if (!((String)p_preset->get("privacy/microphone_usage_description")).is_empty()) {
+				descriptions += "\t<key>NSMicrophoneUsageDescription</key>\n";
+				descriptions += "\t<string>" + (String)p_preset->get("privacy/microphone_usage_description") + "</string>\n";
+			}
+			if (!((String)p_preset->get("privacy/camera_usage_description")).is_empty()) {
+				descriptions += "\t<key>NSCameraUsageDescription</key>\n";
+				descriptions += "\t<string>" + (String)p_preset->get("privacy/camera_usage_description") + "</string>\n";
+			}
+			if (!((String)p_preset->get("privacy/location_usage_description")).is_empty()) {
+				descriptions += "\t<key>NSLocationUsageDescription</key>\n";
+				descriptions += "\t<string>" + (String)p_preset->get("privacy/location_usage_description") + "</string>\n";
+			}
+			if (!((String)p_preset->get("privacy/address_book_usage_description")).is_empty()) {
+				descriptions += "\t<key>NSContactsUsageDescription</key>\n";
+				descriptions += "\t<string>" + (String)p_preset->get("privacy/address_book_usage_description") + "</string>\n";
+			}
+			if (!((String)p_preset->get("privacy/calendar_usage_description")).is_empty()) {
+				descriptions += "\t<key>NSCalendarsUsageDescription</key>\n";
+				descriptions += "\t<string>" + (String)p_preset->get("privacy/calendar_usage_description") + "</string>\n";
+			}
+			if (!((String)p_preset->get("privacy/photos_library_usage_description")).is_empty()) {
+				descriptions += "\t<key>NSPhotoLibraryUsageDescription</key>\n";
+				descriptions += "\t<string>" + (String)p_preset->get("privacy/photos_library_usage_description") + "</string>\n";
+			}
+			if (!((String)p_preset->get("privacy/desktop_folder_usage_description")).is_empty()) {
+				descriptions += "\t<key>NSDesktopFolderUsageDescription</key>\n";
+				descriptions += "\t<string>" + (String)p_preset->get("privacy/desktop_folder_usage_description") + "</string>\n";
+			}
+			if (!((String)p_preset->get("privacy/documents_folder_usage_description")).is_empty()) {
+				descriptions += "\t<key>NSDocumentsFolderUsageDescription</key>\n";
+				descriptions += "\t<string>" + (String)p_preset->get("privacy/documents_folder_usage_description") + "</string>\n";
+			}
+			if (!((String)p_preset->get("privacy/downloads_folder_usage_description")).is_empty()) {
+				descriptions += "\t<key>NSDownloadsFolderUsageDescription</key>\n";
+				descriptions += "\t<string>" + (String)p_preset->get("privacy/downloads_folder_usage_description") + "</string>\n";
+			}
+			if (!((String)p_preset->get("privacy/network_volumes_usage_description")).is_empty()) {
+				descriptions += "\t<key>NSNetworkVolumesUsageDescription</key>\n";
+				descriptions += "\t<string>" + (String)p_preset->get("privacy/network_volumes_usage_description") + "</string>\n";
+			}
+			if (!((String)p_preset->get("privacy/removable_volumes_usage_description")).is_empty()) {
+				descriptions += "\t<key>NSRemovableVolumesUsageDescription</key>\n";
+				descriptions += "\t<string>" + (String)p_preset->get("privacy/removable_volumes_usage_description") + "</string>\n";
+			}
+			if (!descriptions.is_empty()) {
+				strnew += lines[i].replace("$usage_descriptions", descriptions);
+			}
 		} else {
 			strnew += lines[i] + "\n";
 		}
@@ -362,14 +419,16 @@ Error EditorExportPlatformOSX::_notarize(const Ref<EditorExportPreset> &p_preset
 	Error err = OS::get_singleton()->execute("xcrun", args, &str, nullptr, true);
 	ERR_FAIL_COND_V(err != OK, err);
 
-	print_line("altool (" + p_path + "):\n" + str);
+	print_verbose("altool (" + p_path + "):\n" + str);
 	if (str.find("RequestUUID") == -1) {
 		EditorNode::add_io_error("altool: " + str);
 		return FAILED;
 	} else {
-		print_line("Note: The notarization process generally takes less than an hour. When the process is completed, you'll receive an email.");
-		print_line("      You can check progress manually by opening a Terminal and running the following command:");
-		print_line("      \"xcrun altool --notarization-history 0 -u <your email> -p <app-specific pwd>\"");
+		print_line(TTR("Note: The notarization process generally takes less than an hour. When the process is completed, you'll receive an email."));
+		print_line("      " + TTR("You can check progress manually by opening a Terminal and running the following command:"));
+		print_line("          \"xcrun altool --notarization-history 0 -u <your email> -p <app-specific pwd>\"");
+		print_line("      " + TTR("Run the following command to staple notarization ticket to the exported application (optional):"));
+		print_line("          \"xcrun stapler staple <app path>\"");
 	}
 
 #endif
@@ -378,71 +437,91 @@ Error EditorExportPlatformOSX::_notarize(const Ref<EditorExportPreset> &p_preset
 }
 
 Error EditorExportPlatformOSX::_code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path) {
-#ifdef OSX_ENABLED
-	List<String> args;
-
+	bool force_builtin_codesign = EditorSettings::get_singleton()->get("export/macos/force_builtin_codesign");
 	bool ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-");
 
-	if (p_preset->get("codesign/timestamp")) {
-		if (ad_hoc) {
+	if ((!FileAccess::exists("/usr/bin/codesign") && !FileAccess::exists("/bin/codesign")) || force_builtin_codesign) {
+		print_verbose("using built-in codesign...");
+#ifdef MODULE_REGEX_ENABLED
+		if (p_preset->get("codesign/timestamp")) {
 			WARN_PRINT("Timestamping is not compatible with ad-hoc signature, and was disabled!");
-		} else {
-			args.push_back("--timestamp");
 		}
-	}
-	if (p_preset->get("codesign/hardened_runtime")) {
-		if (ad_hoc) {
+		if (p_preset->get("codesign/hardened_runtime")) {
 			WARN_PRINT("Hardened Runtime is not compatible with ad-hoc signature, and was disabled!");
-		} else {
-			args.push_back("--options");
-			args.push_back("runtime");
 		}
-	}
 
-	if (p_path.get_extension() != "dmg") {
-		args.push_back("--entitlements");
-		args.push_back(p_ent_path);
-	}
+		String error_msg;
+		Error err = CodeSign::codesign(false, p_preset->get("codesign/replace_existing_signature"), p_path, p_ent_path, error_msg);
+		if (err != OK) {
+			EditorNode::add_io_error("Built-in CodeSign: " + error_msg);
+			return FAILED;
+		}
+#else
+		ERR_FAIL_V_MSG(FAILED, "Built-in CodeSign require regex module");
+#endif
+		return OK;
+	} else {
+		print_verbose("using external codesign...");
+		List<String> args;
+		if (p_preset->get("codesign/timestamp")) {
+			if (ad_hoc) {
+				WARN_PRINT("Timestamping is not compatible with ad-hoc signature, and was disabled!");
+			} else {
+				args.push_back("--timestamp");
+			}
+		}
+		if (p_preset->get("codesign/hardened_runtime")) {
+			if (ad_hoc) {
+				WARN_PRINT("Hardened Runtime is not compatible with ad-hoc signature, and was disabled!");
+			} else {
+				args.push_back("--options");
+				args.push_back("runtime");
+			}
+		}
 
-	PackedStringArray user_args = p_preset->get("codesign/custom_options");
-	for (int i = 0; i < user_args.size(); i++) {
-		String user_arg = user_args[i].strip_edges();
-		if (!user_arg.is_empty()) {
-			args.push_back(user_arg);
+		if (p_path.get_extension() != "dmg") {
+			args.push_back("--entitlements");
+			args.push_back(p_ent_path);
 		}
-	}
 
-	args.push_back("-s");
-	if (ad_hoc) {
-		args.push_back("-");
-	} else {
-		args.push_back(p_preset->get("codesign/identity"));
-	}
+		PackedStringArray user_args = p_preset->get("codesign/custom_options");
+		for (int i = 0; i < user_args.size(); i++) {
+			String user_arg = user_args[i].strip_edges();
+			if (!user_arg.is_empty()) {
+				args.push_back(user_arg);
+			}
+		}
 
-	args.push_back("-v"); /* provide some more feedback */
+		args.push_back("-s");
+		if (ad_hoc) {
+			args.push_back("-");
+		} else {
+			args.push_back(p_preset->get("codesign/identity"));
+		}
 
-	if (p_preset->get("codesign/replace_existing_signature")) {
-		args.push_back("-f");
-	}
+		args.push_back("-v"); /* provide some more feedback */
 
-	args.push_back(p_path);
+		if (p_preset->get("codesign/replace_existing_signature")) {
+			args.push_back("-f");
+		}
 
-	String str;
-	Error err = OS::get_singleton()->execute("codesign", args, &str, nullptr, true);
-	ERR_FAIL_COND_V(err != OK, err);
+		args.push_back(p_path);
 
-	print_line("codesign (" + p_path + "):\n" + str);
-	if (str.find("no identity found") != -1) {
-		EditorNode::add_io_error("codesign: no identity found");
-		return FAILED;
-	}
-	if ((str.find("unrecognized blob type") != -1) || (str.find("cannot read entitlement data") != -1)) {
-		EditorNode::add_io_error("codesign: invalid entitlements file");
-		return FAILED;
-	}
-#endif
+		String str;
+		Error err = OS::get_singleton()->execute("codesign", args, &str, nullptr, true);
+		ERR_FAIL_COND_V(err != OK, err);
 
-	return OK;
+		print_verbose("codesign (" + p_path + "):\n" + str);
+		if (str.find("no identity found") != -1) {
+			EditorNode::add_io_error("CodeSign: " + TTR("No identity found."));
+			return FAILED;
+		}
+		if ((str.find("unrecognized blob type") != -1) || (str.find("cannot read entitlement data") != -1)) {
+			EditorNode::add_io_error("CodeSign: " + TTR("Invalid entitlements file."));
+			return FAILED;
+		}
+		return OK;
+	}
 }
 
 Error EditorExportPlatformOSX::_code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path,
@@ -560,12 +639,12 @@ Error EditorExportPlatformOSX::_create_dmg(const String &p_dmg_path, const Strin
 	Error err = OS::get_singleton()->execute("hdiutil", args, &str, nullptr, true);
 	ERR_FAIL_COND_V(err != OK, err);
 
-	print_line("hdiutil returned: " + str);
+	print_verbose("hdiutil returned: " + str);
 	if (str.find("create failed") != -1) {
 		if (str.find("File exists") != -1) {
-			EditorNode::add_io_error("hdiutil: create failed - file exists");
+			EditorNode::add_io_error("hdiutil: " + TTR("DMG creation failed, file already exists."));
 		} else {
-			EditorNode::add_io_error("hdiutil: create failed");
+			EditorNode::add_io_error("hdiutil: " + TTR("DMG create failed."));
 		}
 		return FAILED;
 	}
@@ -602,13 +681,13 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
 	FileAccess *src_f = nullptr;
 	zlib_filefunc_def io = zipio_create_io_from_file(&src_f);
 
-	if (ep.step("Creating app", 0)) {
+	if (ep.step(TTR("Creating app bundle"), 0)) {
 		return ERR_SKIP;
 	}
 
 	unzFile src_pkg_zip = unzOpen2(src_pkg_name.utf8().get_data(), &io);
 	if (!src_pkg_zip) {
-		EditorNode::add_io_error("Could not find template app to export:\n" + src_pkg_name);
+		EditorNode::add_io_error(TTR("Could not find template app to export:") + "\n" + src_pkg_name);
 		return ERR_FILE_NOT_FOUND;
 	}
 
@@ -627,12 +706,27 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
 
 	pkg_name = OS::get_singleton()->get_safe_dir_name(pkg_name);
 
-	String export_format = use_dmg() && p_path.ends_with("dmg") ? "dmg" : "zip";
+	String export_format;
+	if (use_dmg() && p_path.ends_with("dmg")) {
+		export_format = "dmg";
+	} else if (p_path.ends_with("zip")) {
+		export_format = "zip";
+	} else if (p_path.ends_with("app")) {
+		export_format = "app";
+	} else {
+		EditorNode::add_io_error("Invalid export format");
+		return ERR_CANT_CREATE;
+	}
 
 	// Create our application bundle.
 	String tmp_app_dir_name = pkg_name + ".app";
-	String tmp_app_path_name = EditorPaths::get_singleton()->get_cache_dir().plus_file(tmp_app_dir_name);
-	print_line("Exporting to " + tmp_app_path_name);
+	String tmp_app_path_name;
+	if (export_format == "app") {
+		tmp_app_path_name = p_path;
+	} else {
+		tmp_app_path_name = EditorPaths::get_singleton()->get_cache_dir().plus_file(tmp_app_dir_name);
+	}
+	print_verbose("Exporting to " + tmp_app_path_name);
 
 	Error err = OK;
 
@@ -641,16 +735,22 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
 		err = ERR_CANT_CREATE;
 	}
 
+	if (DirAccess::exists(tmp_app_dir_name)) {
+		if (tmp_app_dir->change_dir(tmp_app_path_name) == OK) {
+			tmp_app_dir->erase_contents_recursive();
+		}
+	}
+
 	Array helpers = p_preset->get("codesign/entitlements/app_sandbox/helper_executables");
 
 	// Create our folder structure.
 	if (err == OK) {
-		print_line("Creating " + tmp_app_path_name + "/Contents/MacOS");
+		print_verbose("Creating " + tmp_app_path_name + "/Contents/MacOS");
 		err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/MacOS");
 	}
 
 	if (err == OK) {
-		print_line("Creating " + tmp_app_path_name + "/Contents/Frameworks");
+		print_verbose("Creating " + tmp_app_path_name + "/Contents/Frameworks");
 		err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Frameworks");
 	}
 
@@ -660,7 +760,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
 	}
 
 	if (err == OK) {
-		print_line("Creating " + tmp_app_path_name + "/Contents/Resources");
+		print_verbose("Creating " + tmp_app_path_name + "/Contents/Resources");
 		err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Resources");
 	}
 
@@ -689,6 +789,25 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
 		// Write.
 		file = file.replace_first("osx_template.app/", "");
 
+		if (((info.external_fa >> 16L) & 0120000) == 0120000) {
+#ifndef UNIX_ENABLED
+			WARN_PRINT(vformat("Relative symlinks are not supported on this OS, exported project might be broken!"));
+#endif
+			// Handle symlinks in the archive.
+			file = tmp_app_path_name.plus_file(file);
+			if (err == OK) {
+				err = tmp_app_dir->make_dir_recursive(file.get_base_dir());
+			}
+			if (err == OK) {
+				String lnk_data = String::utf8((const char *)data.ptr(), data.size());
+				err = tmp_app_dir->create_link(lnk_data, file);
+				print_verbose(vformat("ADDING SYMLINK %s => %s\n", file, lnk_data));
+			}
+
+			ret = unzGoToNextFile(src_pkg_zip);
+			continue; // next
+		}
+
 		if (file == "Contents/Info.plist") {
 			_fix_plist(p_preset, data, pkg_name);
 		}
@@ -752,7 +871,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
 				dylibs_found.push_back(file);
 			}
 
-			print_line("ADDING: " + file + " size: " + itos(data.size()));
+			print_verbose("ADDING: " + file + " size: " + itos(data.size()));
 
 			// Write it into our application bundle.
 			file = tmp_app_path_name.plus_file(file);
@@ -782,12 +901,12 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
 	unzClose(src_pkg_zip);
 
 	if (!found_binary) {
-		ERR_PRINT("Requested template binary '" + binary_to_use + "' not found. It might be missing from your template archive.");
+		ERR_PRINT(vformat("Requested template binary '%s' not found. It might be missing from your template archive.", binary_to_use));
 		err = ERR_FILE_NOT_FOUND;
 	}
 
 	if (err == OK) {
-		if (ep.step("Making PKG", 1)) {
+		if (ep.step(TTR("Making PKG"), 1)) {
 			return ERR_SKIP;
 		}
 
@@ -965,6 +1084,22 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
 				FileAccess::set_unix_permissions(tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), 0755);
 			}
 		}
+
+		bool ad_hoc = true;
+		if (err == OK) {
+#ifdef OSX_ENABLED
+			String sign_identity = p_preset->get("codesign/identity");
+#else
+			String sign_identity = "-";
+#endif
+			ad_hoc = (sign_identity == "" || sign_identity == "-");
+			bool lib_validation = p_preset->get("codesign/entitlements/disable_library_validation");
+			if ((!dylibs_found.is_empty() || !shared_objects.is_empty()) && sign_enabled && ad_hoc && !lib_validation) {
+				ERR_PRINT("Application with an ad-hoc signature require 'Disable Library Validation' entitlement to load dynamic libraries.");
+				err = ERR_CANT_CREATE;
+			}
+		}
+
 		if (err == OK) {
 			DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
 			for (int i = 0; i < shared_objects.size(); i++) {
@@ -994,31 +1129,31 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
 		}
 
 		if (err == OK && sign_enabled) {
-			if (ep.step("Code signing bundle", 2)) {
+			if (ep.step(TTR("Code signing bundle"), 2)) {
 				return ERR_SKIP;
 			}
-			err = _code_sign(p_preset, tmp_app_path_name + "/Contents/MacOS/" + pkg_name, ent_path);
+			err = _code_sign(p_preset, tmp_app_path_name, ent_path);
 		}
 
 		if (export_format == "dmg") {
 			// Create a DMG.
 			if (err == OK) {
-				if (ep.step("Making DMG", 3)) {
+				if (ep.step(TTR("Making DMG"), 3)) {
 					return ERR_SKIP;
 				}
 				err = _create_dmg(p_path, pkg_name, tmp_app_path_name);
 			}
 			// Sign DMG.
-			if (err == OK && sign_enabled) {
-				if (ep.step("Code signing DMG", 3)) {
+			if (err == OK && sign_enabled && !ad_hoc) {
+				if (ep.step(TTR("Code signing DMG"), 3)) {
 					return ERR_SKIP;
 				}
 				err = _code_sign(p_preset, p_path, ent_path);
 			}
-		} else {
+		} else if (export_format == "zip") {
 			// Create ZIP.
 			if (err == OK) {
-				if (ep.step("Making ZIP", 3)) {
+				if (ep.step(TTR("Making ZIP"), 3)) {
 					return ERR_SKIP;
 				}
 				if (FileAccess::exists(p_path)) {
@@ -1037,20 +1172,30 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
 
 		bool noto_enabled = p_preset->get("notarization/enable");
 		if (err == OK && noto_enabled) {
-			if (ep.step("Sending archive for notarization", 4)) {
-				return ERR_SKIP;
+			if (export_format == "app") {
+				WARN_PRINT("Notarization require app to be archived first, select DMG or ZIP export format instead.");
+			} else {
+				if (ep.step(TTR("Sending archive for notarization"), 4)) {
+					return ERR_SKIP;
+				}
+				err = _notarize(p_preset, p_path);
 			}
-			err = _notarize(p_preset, p_path);
 		}
 
 		// Clean up temporary entitlements files.
 		DirAccess::remove_file_or_error(hlp_ent_path);
 
-		// Clean up temporary .app dir.
-		tmp_app_dir->change_dir(tmp_app_path_name);
-		tmp_app_dir->erase_contents_recursive();
-		tmp_app_dir->change_dir("..");
-		tmp_app_dir->remove(tmp_app_dir_name);
+		// Clean up temporary .app dir and generated entitlements.
+		if ((String)(p_preset->get("codesign/entitlements/custom_file")) == "") {
+			tmp_app_dir->remove(ent_path);
+		}
+		if (export_format != "app") {
+			if (tmp_app_dir->change_dir(tmp_app_path_name) == OK) {
+				tmp_app_dir->erase_contents_recursive();
+				tmp_app_dir->change_dir("..");
+				tmp_app_dir->remove(tmp_app_dir_name);
+			}
+		}
 	}
 
 	return err;
@@ -1152,7 +1297,7 @@ void EditorExportPlatformOSX::_zip_folder_recursive(zipFile &p_zip, const String
 
 			FileAccessRef fa = FileAccess::open(dir.plus_file(f), FileAccess::READ);
 			if (!fa) {
-				ERR_FAIL_MSG("Can't open file to read from path '" + String(dir.plus_file(f)) + "'.");
+				ERR_FAIL_MSG(vformat("Can't open file to read from path \"%s\".", dir.plus_file(f)));
 			}
 			const int bufsize = 16384;
 			uint8_t buf[bufsize];
@@ -1209,11 +1354,19 @@ bool EditorExportPlatformOSX::can_export(const Ref<EditorExportPreset> &p_preset
 		valid = false;
 	}
 
-#ifdef OSX_ENABLED
 	bool sign_enabled = p_preset->get("codesign/enable");
 	bool noto_enabled = p_preset->get("notarization/enable");
 	bool ad_hoc = ((p_preset->get("codesign/identity") == "") || (p_preset->get("codesign/identity") == "-"));
 
+#ifdef OSX_ENABLED
+	if (!ad_hoc && (bool)EditorSettings::get_singleton()->get("export/macos/force_builtin_codesign")) {
+		err += TTR("Warning: Built-in \"codesign\" is selected in the Editor Settings. Code signing is limited to ad-hoc signature only.") + "\n";
+	}
+	if (!ad_hoc && !FileAccess::exists("/usr/bin/codesign") && !FileAccess::exists("/bin/codesign")) {
+		err += TTR("Warning: Xcode command line tools are not installed, using built-in \"codesign\". Code signing is limited to ad-hoc signature only.") + "\n";
+	}
+#endif
+
 	if (noto_enabled) {
 		if (ad_hoc) {
 			err += TTR("Notarization: Notarization with the ad-hoc signature is not supported.") + "\n";
@@ -1240,7 +1393,11 @@ bool EditorExportPlatformOSX::can_export(const Ref<EditorExportPreset> &p_preset
 			valid = false;
 		}
 	} else {
-		err += TTR("Notarization is disabled. Exported project will be blocked by Gatekeeper, if it's downloaded from an unknown source.") + "\n";
+#ifdef OSX_ENABLED
+		err += TTR("Warning: Notarization is disabled. Exported project will be blocked by Gatekeeper, if it's downloaded from an unknown source.") + "\n";
+#else
+		err += TTR("Warning: Notarization is not supported on this OS. Exported project will be blocked by Gatekeeper, if it's downloaded from an unknown source.") + "\n";
+#endif
 		if (!sign_enabled) {
 			err += TTR("Code signing is disabled. Exported project will not run on Macs with enabled Gatekeeper and Apple Silicon powered Macs.") + "\n";
 		} else {
@@ -1252,9 +1409,33 @@ bool EditorExportPlatformOSX::can_export(const Ref<EditorExportPreset> &p_preset
 			}
 		}
 	}
-#else
-	err += TTR("macOS code signing and Notarization is not supported on the host OS. Exported project will not run on Macs with enabled Gatekeeper and Apple Silicon powered Macs.") + "\n";
-#endif
+
+	if (sign_enabled) {
+		if ((bool)p_preset->get("codesign/entitlements/audio_input") && ((String)p_preset->get("privacy/microphone_usage_description")).is_empty()) {
+			err += TTR("Privacy: Microphone access is enabled, but usage description is not specified.") + "\n";
+			valid = false;
+		}
+		if ((bool)p_preset->get("codesign/entitlements/camera") && ((String)p_preset->get("privacy/camera_usage_description")).is_empty()) {
+			err += TTR("Privacy: Camera access is enabled, but usage description is not specified.") + "\n";
+			valid = false;
+		}
+		if ((bool)p_preset->get("codesign/entitlements/location") && ((String)p_preset->get("privacy/location_usage_description")).is_empty()) {
+			err += TTR("Privacy: Location information access is enabled, but usage description is not specified.") + "\n";
+			valid = false;
+		}
+		if ((bool)p_preset->get("codesign/entitlements/address_book") && ((String)p_preset->get("privacy/address_book_usage_description")).is_empty()) {
+			err += TTR("Privacy: Address book access is enabled, but usage description is not specified.") + "\n";
+			valid = false;
+		}
+		if ((bool)p_preset->get("codesign/entitlements/calendars") && ((String)p_preset->get("privacy/calendar_usage_description")).is_empty()) {
+			err += TTR("Privacy: Calendar access is enabled, but usage description is not specified.") + "\n";
+			valid = false;
+		}
+		if ((bool)p_preset->get("codesign/entitlements/photos_library") && ((String)p_preset->get("privacy/photos_library_usage_description")).is_empty()) {
+			err += TTR("Privacy: Photo library access is enabled, but usage description is not specified.") + "\n";
+			valid = false;
+		}
+	}
 
 	if (!err.is_empty()) {
 		r_error = err;

+ 3 - 2
platform/osx/export/export_plugin.h

@@ -68,13 +68,13 @@ class EditorExportPlatformOSX : public EditorExportPlatform {
 	Error _create_dmg(const String &p_dmg_path, const String &p_pkg_name, const String &p_app_path_name);
 	void _zip_folder_recursive(zipFile &p_zip, const String &p_root_path, const String &p_folder, const String &p_pkg_name);
 
-#ifdef OSX_ENABLED
 	bool use_codesign() const { return true; }
+#ifdef OSX_ENABLED
 	bool use_dmg() const { return true; }
 #else
-	bool use_codesign() const { return false; }
 	bool use_dmg() const { return false; }
 #endif
+
 	bool is_package_name_valid(const String &p_package, String *r_error = nullptr) const {
 		String pname = p_package;
 
@@ -113,6 +113,7 @@ public:
 			list.push_back("dmg");
 		}
 		list.push_back("zip");
+		list.push_back("app");
 		return list;
 	}
 	virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override;

+ 243 - 0
platform/osx/export/lipo.cpp

@@ -0,0 +1,243 @@
+/*************************************************************************/
+/*  lipo.cpp                                                             */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#include "modules/modules_enabled.gen.h" // For regex.
+
+#include "lipo.h"
+
+#ifdef MODULE_REGEX_ENABLED
+
+bool LipO::is_lipo(const String &p_path) {
+	FileAccessRef fb = FileAccess::open(p_path, FileAccess::READ);
+	ERR_FAIL_COND_V_MSG(!fb, false, vformat("LipO: Can't open file: \"%s\".", p_path));
+	uint32_t magic = fb->get_32();
+	return (magic == 0xbebafeca || magic == 0xcafebabe || magic == 0xbfbafeca || magic == 0xcafebabf);
+}
+
+bool LipO::create_file(const String &p_output_path, const PackedStringArray &p_files) {
+	close();
+
+	fa = FileAccess::open(p_output_path, FileAccess::WRITE);
+	ERR_FAIL_COND_V_MSG(!fa, false, vformat("LipO: Can't open file: \"%s\".", p_output_path));
+
+	uint64_t max_size = 0;
+	for (int i = 0; i < p_files.size(); i++) {
+		MachO mh;
+		if (!mh.open_file(p_files[i])) {
+			ERR_FAIL_V_MSG(false, vformat("LipO: Invalid MachO file: \"%s.\"", p_files[i]));
+		}
+
+		FatArch arch;
+		arch.cputype = mh.get_cputype();
+		arch.cpusubtype = mh.get_cpusubtype();
+		arch.offset = 0;
+		arch.size = mh.get_size();
+		arch.align = mh.get_align();
+		max_size += arch.size;
+
+		archs.push_back(arch);
+
+		FileAccessRef fb = FileAccess::open(p_files[i], FileAccess::READ);
+		if (!fb) {
+			close();
+			ERR_FAIL_V_MSG(false, vformat("LipO: Can't open file: \"%s.\"", p_files[i]));
+		}
+	}
+
+	// Write header.
+	bool is_64 = (max_size >= std::numeric_limits<uint32_t>::max());
+	if (is_64) {
+		fa->store_32(0xbfbafeca);
+	} else {
+		fa->store_32(0xbebafeca);
+	}
+	fa->store_32(BSWAP32(archs.size()));
+	uint64_t offset = archs.size() * (is_64 ? 32 : 20) + 8;
+	for (int i = 0; i < archs.size(); i++) {
+		archs.write[i].offset = offset + PAD(offset, uint64_t(1) << archs[i].align);
+		if (is_64) {
+			fa->store_32(BSWAP32(archs[i].cputype));
+			fa->store_32(BSWAP32(archs[i].cpusubtype));
+			fa->store_64(BSWAP64(archs[i].offset));
+			fa->store_64(BSWAP64(archs[i].size));
+			fa->store_32(BSWAP32(archs[i].align));
+			fa->store_32(0);
+		} else {
+			fa->store_32(BSWAP32(archs[i].cputype));
+			fa->store_32(BSWAP32(archs[i].cpusubtype));
+			fa->store_32(BSWAP32(archs[i].offset));
+			fa->store_32(BSWAP32(archs[i].size));
+			fa->store_32(BSWAP32(archs[i].align));
+		}
+		offset = archs[i].offset + archs[i].size;
+	}
+
+	// Write files and padding.
+	for (int i = 0; i < archs.size(); i++) {
+		FileAccessRef fb = FileAccess::open(p_files[i], FileAccess::READ);
+		if (!fb) {
+			close();
+			ERR_FAIL_V_MSG(false, vformat("LipO: Can't open file: \"%s.\"", p_files[i]));
+		}
+		uint64_t cur = fa->get_position();
+		for (uint64_t j = cur; j < archs[i].offset; j++) {
+			fa->store_8(0);
+		}
+		int pages = archs[i].size / 4096;
+		int remain = archs[i].size % 4096;
+		unsigned char step[4096];
+		for (int j = 0; j < pages; j++) {
+			uint64_t br = fb->get_buffer(step, 4096);
+			if (br > 0) {
+				fa->store_buffer(step, br);
+			}
+		}
+		uint64_t br = fb->get_buffer(step, remain);
+		if (br > 0) {
+			fa->store_buffer(step, br);
+		}
+		fb->close();
+	}
+	return true;
+}
+
+bool LipO::open_file(const String &p_path) {
+	close();
+
+	fa = FileAccess::open(p_path, FileAccess::READ);
+	ERR_FAIL_COND_V_MSG(!fa, false, vformat("LipO: Can't open file: \"%s\".", p_path));
+
+	uint32_t magic = fa->get_32();
+	if (magic == 0xbebafeca) {
+		// 32-bit fat binary, bswap.
+		uint32_t nfat_arch = BSWAP32(fa->get_32());
+		for (uint32_t i = 0; i < nfat_arch; i++) {
+			FatArch arch;
+			arch.cputype = BSWAP32(fa->get_32());
+			arch.cpusubtype = BSWAP32(fa->get_32());
+			arch.offset = BSWAP32(fa->get_32());
+			arch.size = BSWAP32(fa->get_32());
+			arch.align = BSWAP32(fa->get_32());
+
+			archs.push_back(arch);
+		}
+	} else if (magic == 0xcafebabe) {
+		// 32-bit fat binary.
+		uint32_t nfat_arch = fa->get_32();
+		for (uint32_t i = 0; i < nfat_arch; i++) {
+			FatArch arch;
+			arch.cputype = fa->get_32();
+			arch.cpusubtype = fa->get_32();
+			arch.offset = fa->get_32();
+			arch.size = fa->get_32();
+			arch.align = fa->get_32();
+
+			archs.push_back(arch);
+		}
+	} else if (magic == 0xbfbafeca) {
+		// 64-bit fat binary, bswap.
+		uint32_t nfat_arch = BSWAP32(fa->get_32());
+		for (uint32_t i = 0; i < nfat_arch; i++) {
+			FatArch arch;
+			arch.cputype = BSWAP32(fa->get_32());
+			arch.cpusubtype = BSWAP32(fa->get_32());
+			arch.offset = BSWAP64(fa->get_64());
+			arch.size = BSWAP64(fa->get_64());
+			arch.align = BSWAP32(fa->get_32());
+			fa->get_32(); // Skip, reserved.
+
+			archs.push_back(arch);
+		}
+	} else if (magic == 0xcafebabf) {
+		// 64-bit fat binary.
+		uint32_t nfat_arch = fa->get_32();
+		for (uint32_t i = 0; i < nfat_arch; i++) {
+			FatArch arch;
+			arch.cputype = fa->get_32();
+			arch.cpusubtype = fa->get_32();
+			arch.offset = fa->get_64();
+			arch.size = fa->get_64();
+			arch.align = fa->get_32();
+			fa->get_32(); // Skip, reserved.
+
+			archs.push_back(arch);
+		}
+	} else {
+		close();
+		ERR_FAIL_V_MSG(false, vformat("LipO: Invalid fat binary: \"%s\".", p_path));
+	}
+	return true;
+}
+
+int LipO::get_arch_count() const {
+	ERR_FAIL_COND_V_MSG(!fa, 0, "LipO: File not opened.");
+	return archs.size();
+}
+
+bool LipO::extract_arch(int p_index, const String &p_path) {
+	ERR_FAIL_COND_V_MSG(!fa, false, "LipO: File not opened.");
+	ERR_FAIL_INDEX_V(p_index, archs.size(), false);
+
+	FileAccessRef fb = FileAccess::open(p_path, FileAccess::WRITE);
+	ERR_FAIL_COND_V_MSG(!fb, false, vformat("LipO: Can't open file: \"%s\".", p_path));
+
+	fa->seek(archs[p_index].offset);
+
+	int pages = archs[p_index].size / 4096;
+	int remain = archs[p_index].size % 4096;
+	unsigned char step[4096];
+	for (int i = 0; i < pages; i++) {
+		uint64_t br = fa->get_buffer(step, 4096);
+		if (br > 0) {
+			fb->store_buffer(step, br);
+		}
+	}
+	uint64_t br = fa->get_buffer(step, remain);
+	if (br > 0) {
+		fb->store_buffer(step, br);
+	}
+	fb->close();
+	return true;
+}
+
+void LipO::close() {
+	if (fa) {
+		fa->close();
+		memdelete(fa);
+		fa = nullptr;
+	}
+	archs.clear();
+}
+
+LipO::~LipO() {
+	close();
+}
+
+#endif // MODULE_REGEX_ENABLED

+ 76 - 0
platform/osx/export/lipo.h

@@ -0,0 +1,76 @@
+/*************************************************************************/
+/*  lipo.h                                                               */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+// Universal / Universal 2 fat binary file creator and extractor.
+
+#ifndef LIPO_H
+#define LIPO_H
+
+#include "core/io/file_access.h"
+#include "core/object/ref_counted.h"
+#include "modules/modules_enabled.gen.h" // For regex.
+
+#include "macho.h"
+
+#ifdef MODULE_REGEX_ENABLED
+
+class LipO : public RefCounted {
+	struct FatArch {
+		uint32_t cputype;
+		uint32_t cpusubtype;
+		uint64_t offset;
+		uint64_t size;
+		uint32_t align;
+	};
+
+	FileAccess *fa = nullptr;
+	Vector<FatArch> archs;
+
+	static inline size_t PAD(size_t s, size_t a) {
+		return (a - s % a);
+	}
+
+public:
+	static bool is_lipo(const String &p_path);
+
+	bool create_file(const String &p_output_path, const PackedStringArray &p_files);
+
+	bool open_file(const String &p_path);
+	int get_arch_count() const;
+	bool extract_arch(int p_index, const String &p_path);
+
+	void close();
+
+	~LipO();
+};
+
+#endif // MODULE_REGEX_ENABLED
+
+#endif // LIPO_H

+ 556 - 0
platform/osx/export/macho.cpp

@@ -0,0 +1,556 @@
+/*************************************************************************/
+/*  macho.cpp                                                            */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#include "modules/modules_enabled.gen.h" // For regex.
+
+#include "macho.h"
+
+#ifdef MODULE_REGEX_ENABLED
+
+uint32_t MachO::seg_align(uint64_t p_vmaddr, uint32_t p_min, uint32_t p_max) {
+	uint32_t align = p_max;
+	if (p_vmaddr != 0) {
+		uint64_t seg_align = 1;
+		align = 0;
+		while ((seg_align & p_vmaddr) == 0) {
+			seg_align = seg_align << 1;
+			align++;
+		}
+		align = CLAMP(align, p_min, p_max);
+	}
+	return align;
+}
+
+bool MachO::alloc_signature(uint64_t p_size) {
+	ERR_FAIL_COND_V_MSG(!fa, false, "MachO: File not opened.");
+	if (signature_offset != 0) {
+		// Nothing to do, already have signature load command.
+		return true;
+	}
+	if (lc_limit == 0 || lc_limit + 16 > exe_base) {
+		ERR_FAIL_V_MSG(false, "MachO: Can't allocate signature load command, please use \"codesign_allocate\" utility first.");
+	} else {
+		// Add signature load command.
+		signature_offset = lc_limit;
+
+		fa->seek(lc_limit);
+		LoadCommandHeader lc;
+		lc.cmd = LC_CODE_SIGNATURE;
+		lc.cmdsize = 16;
+		if (swap) {
+			lc.cmdsize = BSWAP32(lc.cmdsize);
+		}
+		fa->store_buffer((const uint8_t *)&lc, sizeof(LoadCommandHeader));
+
+		uint32_t lc_offset = fa->get_length() + PAD(fa->get_length(), 16);
+		uint32_t lc_size = 0;
+		if (swap) {
+			lc_offset = BSWAP32(lc_offset);
+			lc_size = BSWAP32(lc_size);
+		}
+		fa->store_32(lc_offset);
+		fa->store_32(lc_size);
+
+		// Write new command number.
+		fa->seek(0x10);
+		uint32_t ncmds = fa->get_32();
+		uint32_t cmdssize = fa->get_32();
+		if (swap) {
+			ncmds = BSWAP32(ncmds);
+			cmdssize = BSWAP32(cmdssize);
+		}
+		ncmds += 1;
+		cmdssize += 16;
+		if (swap) {
+			ncmds = BSWAP32(ncmds);
+			cmdssize = BSWAP32(cmdssize);
+		}
+		fa->seek(0x10);
+		fa->store_32(ncmds);
+		fa->store_32(cmdssize);
+
+		lc_limit = lc_limit + sizeof(LoadCommandHeader) + 8;
+
+		return true;
+	}
+}
+
+bool MachO::is_macho(const String &p_path) {
+	FileAccessRef fb = FileAccess::open(p_path, FileAccess::READ);
+	ERR_FAIL_COND_V_MSG(!fb, false, vformat("MachO: Can't open file: \"%s\".", p_path));
+	uint32_t magic = fb->get_32();
+	return (magic == 0xcefaedfe || magic == 0xfeedface || magic == 0xcffaedfe || magic == 0xfeedfacf);
+}
+
+bool MachO::open_file(const String &p_path) {
+	fa = FileAccess::open(p_path, FileAccess::READ_WRITE);
+	ERR_FAIL_COND_V_MSG(!fa, false, vformat("MachO: Can't open file: \"%s\".", p_path));
+	uint32_t magic = fa->get_32();
+	MachHeader mach_header;
+
+	// Read MachO header.
+	swap = (magic == 0xcffaedfe || magic == 0xcefaedfe);
+	if (magic == 0xcefaedfe || magic == 0xfeedface) {
+		// Thin 32-bit binary.
+		fa->get_buffer((uint8_t *)&mach_header, sizeof(MachHeader));
+	} else if (magic == 0xcffaedfe || magic == 0xfeedfacf) {
+		// Thin 64-bit binary.
+		fa->get_buffer((uint8_t *)&mach_header, sizeof(MachHeader));
+		fa->get_32(); // Skip extra reserved field.
+	} else {
+		ERR_FAIL_V_MSG(false, vformat("MachO: File is not a valid MachO binary: \"%s\".", p_path));
+	}
+
+	if (swap) {
+		mach_header.ncmds = BSWAP32(mach_header.ncmds);
+		mach_header.cpusubtype = BSWAP32(mach_header.cpusubtype);
+		mach_header.cputype = BSWAP32(mach_header.cputype);
+	}
+	cpusubtype = mach_header.cpusubtype;
+	cputype = mach_header.cputype;
+	align = 0;
+	exe_base = std::numeric_limits<uint64_t>::max();
+	exe_limit = 0;
+	lc_limit = 0;
+	link_edit_offset = 0;
+	signature_offset = 0;
+
+	// Read load commands.
+	for (uint32_t i = 0; i < mach_header.ncmds; i++) {
+		LoadCommandHeader lc;
+		fa->get_buffer((uint8_t *)&lc, sizeof(LoadCommandHeader));
+		if (swap) {
+			lc.cmd = BSWAP32(lc.cmd);
+			lc.cmdsize = BSWAP32(lc.cmdsize);
+		}
+		uint64_t ps = fa->get_position();
+		switch (lc.cmd) {
+			case LC_SEGMENT: {
+				LoadCommandSegment lc_seg;
+				fa->get_buffer((uint8_t *)&lc_seg, sizeof(LoadCommandSegment));
+				if (swap) {
+					lc_seg.nsects = BSWAP32(lc_seg.nsects);
+					lc_seg.vmaddr = BSWAP32(lc_seg.vmaddr);
+					lc_seg.vmsize = BSWAP32(lc_seg.vmsize);
+				}
+				align = MAX(align, seg_align(lc_seg.vmaddr, 2, 15));
+				if (String(lc_seg.segname) == "__TEXT") {
+					exe_limit = MAX(exe_limit, lc_seg.vmsize);
+					for (uint32_t j = 0; j < lc_seg.nsects; j++) {
+						Section lc_sect;
+						fa->get_buffer((uint8_t *)&lc_sect, sizeof(Section));
+						if (String(lc_sect.sectname) == "__text") {
+							if (swap) {
+								exe_base = MIN(exe_base, BSWAP32(lc_sect.offset));
+							} else {
+								exe_base = MIN(exe_base, lc_sect.offset);
+							}
+						}
+						if (swap) {
+							align = MAX(align, BSWAP32(lc_sect.align));
+						} else {
+							align = MAX(align, lc_sect.align);
+						}
+					}
+				} else if (String(lc_seg.segname) == "__LINKEDIT") {
+					link_edit_offset = ps - 8;
+				}
+			} break;
+			case LC_SEGMENT_64: {
+				LoadCommandSegment64 lc_seg;
+				fa->get_buffer((uint8_t *)&lc_seg, sizeof(LoadCommandSegment64));
+				if (swap) {
+					lc_seg.nsects = BSWAP32(lc_seg.nsects);
+					lc_seg.vmaddr = BSWAP64(lc_seg.vmaddr);
+					lc_seg.vmsize = BSWAP64(lc_seg.vmsize);
+				}
+				align = MAX(align, seg_align(lc_seg.vmaddr, 3, 15));
+				if (String(lc_seg.segname) == "__TEXT") {
+					exe_limit = MAX(exe_limit, lc_seg.vmsize);
+					for (uint32_t j = 0; j < lc_seg.nsects; j++) {
+						Section64 lc_sect;
+						fa->get_buffer((uint8_t *)&lc_sect, sizeof(Section64));
+						if (String(lc_sect.sectname) == "__text") {
+							if (swap) {
+								exe_base = MIN(exe_base, BSWAP32(lc_sect.offset));
+							} else {
+								exe_base = MIN(exe_base, lc_sect.offset);
+							}
+							if (swap) {
+								align = MAX(align, BSWAP32(lc_sect.align));
+							} else {
+								align = MAX(align, lc_sect.align);
+							}
+						}
+					}
+				} else if (String(lc_seg.segname) == "__LINKEDIT") {
+					link_edit_offset = ps - 8;
+				}
+			} break;
+			case LC_CODE_SIGNATURE: {
+				signature_offset = ps - 8;
+			} break;
+			default: {
+			} break;
+		}
+		fa->seek(ps + lc.cmdsize - 8);
+		lc_limit = ps + lc.cmdsize - 8;
+	}
+
+	if (exe_limit == 0 || lc_limit == 0) {
+		ERR_FAIL_V_MSG(false, vformat("MachO: No load commands or executable code found: \"%s\".", p_path));
+	}
+
+	return true;
+}
+
+uint64_t MachO::get_exe_base() {
+	ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened.");
+	return exe_base;
+}
+
+uint64_t MachO::get_exe_limit() {
+	ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened.");
+	return exe_limit;
+}
+
+int32_t MachO::get_align() {
+	ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened.");
+	return align;
+}
+
+uint32_t MachO::get_cputype() {
+	ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened.");
+	return cputype;
+}
+
+uint32_t MachO::get_cpusubtype() {
+	ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened.");
+	return cpusubtype;
+}
+
+uint64_t MachO::get_size() {
+	ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened.");
+	return fa->get_length();
+}
+
+uint64_t MachO::get_signature_offset() {
+	ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened.");
+	ERR_FAIL_COND_V_MSG(signature_offset == 0, 0, "MachO: No signature load command.");
+
+	fa->seek(signature_offset + 8);
+	if (swap) {
+		return BSWAP32(fa->get_32());
+	} else {
+		return fa->get_32();
+	}
+}
+
+uint64_t MachO::get_code_limit() {
+	ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened.");
+
+	if (signature_offset == 0) {
+		return fa->get_length() + PAD(fa->get_length(), 16);
+	} else {
+		return get_signature_offset();
+	}
+}
+
+uint64_t MachO::get_signature_size() {
+	ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened.");
+	ERR_FAIL_COND_V_MSG(signature_offset == 0, 0, "MachO: No signature load command.");
+
+	fa->seek(signature_offset + 12);
+	if (swap) {
+		return BSWAP32(fa->get_32());
+	} else {
+		return fa->get_32();
+	}
+}
+
+bool MachO::is_signed() {
+	ERR_FAIL_COND_V_MSG(!fa, false, "MachO: File not opened.");
+	if (signature_offset == 0) {
+		return false;
+	}
+
+	fa->seek(get_signature_offset());
+	uint32_t magic = BSWAP32(fa->get_32());
+	if (magic != 0xfade0cc0) {
+		return false; // No SuperBlob found.
+	}
+	fa->get_32(); // Skip size field, unused.
+	uint32_t count = BSWAP32(fa->get_32());
+	for (uint32_t i = 0; i < count; i++) {
+		uint32_t index_type = BSWAP32(fa->get_32());
+		uint32_t offset = BSWAP32(fa->get_32());
+		if (index_type == 0x00000000) { // CodeDirectory index type.
+			fa->seek(get_signature_offset() + offset + 12);
+			uint32_t flags = BSWAP32(fa->get_32());
+			if (flags & 0x20000) {
+				return false; // Found CD, linker-signed.
+			} else {
+				return true; // Found CD, not linker-signed.
+			}
+		}
+	}
+	return false; // No CD found.
+}
+
+PackedByteArray MachO::get_cdhash_sha1() {
+	ERR_FAIL_COND_V_MSG(!fa, PackedByteArray(), "MachO: File not opened.");
+	if (signature_offset == 0) {
+		return PackedByteArray();
+	}
+
+	fa->seek(get_signature_offset());
+	uint32_t magic = BSWAP32(fa->get_32());
+	if (magic != 0xfade0cc0) {
+		return PackedByteArray(); // No SuperBlob found.
+	}
+	fa->get_32(); // Skip size field, unused.
+	uint32_t count = BSWAP32(fa->get_32());
+	for (uint32_t i = 0; i < count; i++) {
+		fa->get_32(); // Index type, skip.
+		uint32_t offset = BSWAP32(fa->get_32());
+		uint64_t pos = fa->get_position();
+
+		fa->seek(get_signature_offset() + offset);
+		uint32_t cdmagic = BSWAP32(fa->get_32());
+		uint32_t cdsize = BSWAP32(fa->get_32());
+		if (cdmagic == 0xfade0c02) { // CodeDirectory.
+			fa->seek(get_signature_offset() + offset + 36);
+			uint8_t hash_size = fa->get_8();
+			uint8_t hash_type = fa->get_8();
+			if (hash_size == 0x14 && hash_type == 0x01) { /* SHA-1 */
+				PackedByteArray hash;
+				hash.resize(0x14);
+
+				fa->seek(get_signature_offset() + offset);
+				PackedByteArray blob;
+				blob.resize(cdsize);
+				fa->get_buffer(blob.ptrw(), cdsize);
+
+				CryptoCore::SHA1Context ctx;
+				ctx.start();
+				ctx.update(blob.ptr(), blob.size());
+				ctx.finish(hash.ptrw());
+
+				return hash;
+			}
+		}
+		fa->seek(pos);
+	}
+	return PackedByteArray();
+}
+
+PackedByteArray MachO::get_cdhash_sha256() {
+	ERR_FAIL_COND_V_MSG(!fa, PackedByteArray(), "MachO: File not opened.");
+	if (signature_offset == 0) {
+		return PackedByteArray();
+	}
+
+	fa->seek(get_signature_offset());
+	uint32_t magic = BSWAP32(fa->get_32());
+	if (magic != 0xfade0cc0) {
+		return PackedByteArray(); // No SuperBlob found.
+	}
+	fa->get_32(); // Skip size field, unused.
+	uint32_t count = BSWAP32(fa->get_32());
+	for (uint32_t i = 0; i < count; i++) {
+		fa->get_32(); // Index type, skip.
+		uint32_t offset = BSWAP32(fa->get_32());
+		uint64_t pos = fa->get_position();
+
+		fa->seek(get_signature_offset() + offset);
+		uint32_t cdmagic = BSWAP32(fa->get_32());
+		uint32_t cdsize = BSWAP32(fa->get_32());
+		if (cdmagic == 0xfade0c02) { // CodeDirectory.
+			fa->seek(get_signature_offset() + offset + 36);
+			uint8_t hash_size = fa->get_8();
+			uint8_t hash_type = fa->get_8();
+			if (hash_size == 0x20 && hash_type == 0x02) { /* SHA-256 */
+				PackedByteArray hash;
+				hash.resize(0x20);
+
+				fa->seek(get_signature_offset() + offset);
+				PackedByteArray blob;
+				blob.resize(cdsize);
+				fa->get_buffer(blob.ptrw(), cdsize);
+
+				CryptoCore::SHA256Context ctx;
+				ctx.start();
+				ctx.update(blob.ptr(), blob.size());
+				ctx.finish(hash.ptrw());
+
+				return hash;
+			}
+		}
+		fa->seek(pos);
+	}
+	return PackedByteArray();
+}
+
+PackedByteArray MachO::get_requirements() {
+	ERR_FAIL_COND_V_MSG(!fa, PackedByteArray(), "MachO: File not opened.");
+	if (signature_offset == 0) {
+		return PackedByteArray();
+	}
+
+	fa->seek(get_signature_offset());
+	uint32_t magic = BSWAP32(fa->get_32());
+	if (magic != 0xfade0cc0) {
+		return PackedByteArray(); // No SuperBlob found.
+	}
+	fa->get_32(); // Skip size field, unused.
+	uint32_t count = BSWAP32(fa->get_32());
+	for (uint32_t i = 0; i < count; i++) {
+		fa->get_32(); // Index type, skip.
+		uint32_t offset = BSWAP32(fa->get_32());
+		uint64_t pos = fa->get_position();
+
+		fa->seek(get_signature_offset() + offset);
+		uint32_t rqmagic = BSWAP32(fa->get_32());
+		uint32_t rqsize = BSWAP32(fa->get_32());
+		if (rqmagic == 0xfade0c01) { // Requirements.
+			PackedByteArray blob;
+			fa->seek(get_signature_offset() + offset);
+			blob.resize(rqsize);
+			fa->get_buffer(blob.ptrw(), rqsize);
+			return blob;
+		}
+		fa->seek(pos);
+	}
+	return PackedByteArray();
+}
+
+const FileAccess *MachO::get_file() const {
+	return fa;
+}
+
+FileAccess *MachO::get_file() {
+	return fa;
+}
+
+bool MachO::set_signature_size(uint64_t p_size) {
+	ERR_FAIL_COND_V_MSG(!fa, false, "MachO: File not opened.");
+
+	// Ensure signature load command exists.
+	ERR_FAIL_COND_V_MSG(link_edit_offset == 0, false, "MachO: No __LINKEDIT segment found.");
+	ERR_FAIL_COND_V_MSG(!alloc_signature(p_size), false, "MachO: Can't allocate signature load command.");
+
+	// Update signature load command.
+	uint64_t old_size = get_signature_size();
+	uint64_t new_size = p_size + PAD(p_size, 16384);
+
+	if (new_size <= old_size) {
+		fa->seek(get_signature_offset());
+		for (uint64_t i = 0; i < old_size; i++) {
+			fa->store_8(0x00);
+		}
+		return true;
+	}
+
+	fa->seek(signature_offset + 12);
+	if (swap) {
+		fa->store_32(BSWAP32(new_size));
+	} else {
+		fa->store_32(new_size);
+	}
+
+	uint64_t end = get_signature_offset() + new_size;
+
+	// Update "__LINKEDIT" segment.
+	LoadCommandHeader lc;
+	fa->seek(link_edit_offset);
+	fa->get_buffer((uint8_t *)&lc, sizeof(LoadCommandHeader));
+	if (swap) {
+		lc.cmd = BSWAP32(lc.cmd);
+		lc.cmdsize = BSWAP32(lc.cmdsize);
+	}
+	switch (lc.cmd) {
+		case LC_SEGMENT: {
+			LoadCommandSegment lc_seg;
+			fa->get_buffer((uint8_t *)&lc_seg, sizeof(LoadCommandSegment));
+			if (swap) {
+				lc_seg.vmsize = BSWAP32(lc_seg.vmsize);
+				lc_seg.filesize = BSWAP32(lc_seg.filesize);
+				lc_seg.fileoff = BSWAP32(lc_seg.fileoff);
+			}
+
+			lc_seg.vmsize = end - lc_seg.fileoff;
+			lc_seg.vmsize += PAD(lc_seg.vmsize, 4096);
+			lc_seg.filesize = end - lc_seg.fileoff;
+
+			if (swap) {
+				lc_seg.vmsize = BSWAP32(lc_seg.vmsize);
+				lc_seg.filesize = BSWAP32(lc_seg.filesize);
+			}
+			fa->seek(link_edit_offset + 8);
+			fa->store_buffer((const uint8_t *)&lc_seg, sizeof(LoadCommandSegment));
+		} break;
+		case LC_SEGMENT_64: {
+			LoadCommandSegment64 lc_seg;
+			fa->get_buffer((uint8_t *)&lc_seg, sizeof(LoadCommandSegment64));
+			if (swap) {
+				lc_seg.vmsize = BSWAP64(lc_seg.vmsize);
+				lc_seg.filesize = BSWAP64(lc_seg.filesize);
+				lc_seg.fileoff = BSWAP64(lc_seg.fileoff);
+			}
+			lc_seg.vmsize = end - lc_seg.fileoff;
+			lc_seg.vmsize += PAD(lc_seg.vmsize, 4096);
+			lc_seg.filesize = end - lc_seg.fileoff;
+			if (swap) {
+				lc_seg.vmsize = BSWAP64(lc_seg.vmsize);
+				lc_seg.filesize = BSWAP64(lc_seg.filesize);
+			}
+			fa->seek(link_edit_offset + 8);
+			fa->store_buffer((const uint8_t *)&lc_seg, sizeof(LoadCommandSegment64));
+		} break;
+		default: {
+			ERR_FAIL_V_MSG(false, "MachO: Invalid __LINKEDIT segment type.");
+		} break;
+	}
+	fa->seek(get_signature_offset());
+	for (uint64_t i = 0; i < new_size; i++) {
+		fa->store_8(0x00);
+	}
+	return true;
+}
+
+MachO::~MachO() {
+	if (fa) {
+		fa->close();
+		memdelete(fa);
+		fa = nullptr;
+	}
+}
+
+#endif // MODULE_REGEX_ENABLED

+ 217 - 0
platform/osx/export/macho.h

@@ -0,0 +1,217 @@
+/*************************************************************************/
+/*  macho.h                                                              */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+// Mach-O binary object file format parser and editor.
+
+#ifndef MACHO_H
+#define MACHO_H
+
+#include "core/crypto/crypto.h"
+#include "core/crypto/crypto_core.h"
+#include "core/io/file_access.h"
+#include "core/object/ref_counted.h"
+#include "modules/modules_enabled.gen.h" // For regex.
+
+#ifdef MODULE_REGEX_ENABLED
+
+class MachO : public RefCounted {
+	struct MachHeader {
+		uint32_t cputype;
+		uint32_t cpusubtype;
+		uint32_t filetype;
+		uint32_t ncmds;
+		uint32_t sizeofcmds;
+		uint32_t flags;
+	};
+
+	enum LoadCommandID {
+		LC_SEGMENT = 0x00000001,
+		LC_SYMTAB = 0x00000002,
+		LC_SYMSEG = 0x00000003,
+		LC_THREAD = 0x00000004,
+		LC_UNIXTHREAD = 0x00000005,
+		LC_LOADFVMLIB = 0x00000006,
+		LC_IDFVMLIB = 0x00000007,
+		LC_IDENT = 0x00000008,
+		LC_FVMFILE = 0x00000009,
+		LC_PREPAGE = 0x0000000a,
+		LC_DYSYMTAB = 0x0000000b,
+		LC_LOAD_DYLIB = 0x0000000c,
+		LC_ID_DYLIB = 0x0000000d,
+		LC_LOAD_DYLINKER = 0x0000000e,
+		LC_ID_DYLINKER = 0x0000000f,
+		LC_PREBOUND_DYLIB = 0x00000010,
+		LC_ROUTINES = 0x00000011,
+		LC_SUB_FRAMEWORK = 0x00000012,
+		LC_SUB_UMBRELLA = 0x00000013,
+		LC_SUB_CLIENT = 0x00000014,
+		LC_SUB_LIBRARY = 0x00000015,
+		LC_TWOLEVEL_HINTS = 0x00000016,
+		LC_PREBIND_CKSUM = 0x00000017,
+		LC_LOAD_WEAK_DYLIB = 0x80000018,
+		LC_SEGMENT_64 = 0x00000019,
+		LC_ROUTINES_64 = 0x0000001a,
+		LC_UUID = 0x0000001b,
+		LC_RPATH = 0x8000001c,
+		LC_CODE_SIGNATURE = 0x0000001d,
+		LC_SEGMENT_SPLIT_INFO = 0x0000001e,
+		LC_REEXPORT_DYLIB = 0x8000001f,
+		LC_LAZY_LOAD_DYLIB = 0x00000020,
+		LC_ENCRYPTION_INFO = 0x00000021,
+		LC_DYLD_INFO = 0x00000022,
+		LC_DYLD_INFO_ONLY = 0x80000022,
+		LC_LOAD_UPWARD_DYLIB = 0x80000023,
+		LC_VERSION_MIN_MACOSX = 0x00000024,
+		LC_VERSION_MIN_IPHONEOS = 0x00000025,
+		LC_FUNCTION_STARTS = 0x00000026,
+		LC_DYLD_ENVIRONMENT = 0x00000027,
+		LC_MAIN = 0x80000028,
+		LC_DATA_IN_CODE = 0x00000029,
+		LC_SOURCE_VERSION = 0x0000002a,
+		LC_DYLIB_CODE_SIGN_DRS = 0x0000002b,
+		LC_ENCRYPTION_INFO_64 = 0x0000002c,
+		LC_LINKER_OPTION = 0x0000002d,
+		LC_LINKER_OPTIMIZATION_HINT = 0x0000002e,
+		LC_VERSION_MIN_TVOS = 0x0000002f,
+		LC_VERSION_MIN_WATCHOS = 0x00000030,
+	};
+
+	struct LoadCommandHeader {
+		uint32_t cmd;
+		uint32_t cmdsize;
+	};
+
+	struct LoadCommandSegment {
+		char segname[16];
+		uint32_t vmaddr;
+		uint32_t vmsize;
+		uint32_t fileoff;
+		uint32_t filesize;
+		uint32_t maxprot;
+		uint32_t initprot;
+		uint32_t nsects;
+		uint32_t flags;
+	};
+
+	struct LoadCommandSegment64 {
+		char segname[16];
+		uint64_t vmaddr;
+		uint64_t vmsize;
+		uint64_t fileoff;
+		uint64_t filesize;
+		uint32_t maxprot;
+		uint32_t initprot;
+		uint32_t nsects;
+		uint32_t flags;
+	};
+
+	struct Section {
+		char sectname[16];
+		char segname[16];
+		uint32_t addr;
+		uint32_t size;
+		uint32_t offset;
+		uint32_t align;
+		uint32_t reloff;
+		uint32_t nreloc;
+		uint32_t flags;
+		uint32_t reserved1;
+		uint32_t reserved2;
+	};
+
+	struct Section64 {
+		char sectname[16];
+		char segname[16];
+		uint64_t addr;
+		uint64_t size;
+		uint32_t offset;
+		uint32_t align;
+		uint32_t reloff;
+		uint32_t nreloc;
+		uint32_t flags;
+		uint32_t reserved1;
+		uint32_t reserved2;
+		uint32_t reserved3;
+	};
+
+	FileAccess *fa = nullptr;
+	bool swap = false;
+
+	uint64_t lc_limit = 0;
+
+	uint64_t exe_limit = 0;
+	uint64_t exe_base = std::numeric_limits<uint64_t>::max(); // Start of first __text section.
+	uint32_t align = 0;
+	uint32_t cputype = 0;
+	uint32_t cpusubtype = 0;
+
+	uint64_t link_edit_offset = 0; // __LINKEDIT segment offset.
+	uint64_t signature_offset = 0; // Load command offset.
+
+	uint32_t seg_align(uint64_t p_vmaddr, uint32_t p_min, uint32_t p_max);
+	bool alloc_signature(uint64_t p_size);
+
+	static inline size_t PAD(size_t s, size_t a) {
+		return (a - s % a);
+	}
+
+public:
+	static bool is_macho(const String &p_path);
+
+	bool open_file(const String &p_path);
+
+	uint64_t get_exe_base();
+	uint64_t get_exe_limit();
+	int32_t get_align();
+	uint32_t get_cputype();
+	uint32_t get_cpusubtype();
+	uint64_t get_size();
+	uint64_t get_code_limit();
+
+	uint64_t get_signature_offset();
+	bool is_signed();
+
+	PackedByteArray get_cdhash_sha1();
+	PackedByteArray get_cdhash_sha256();
+
+	PackedByteArray get_requirements();
+
+	const FileAccess *get_file() const;
+	FileAccess *get_file();
+
+	uint64_t get_signature_size();
+	bool set_signature_size(uint64_t p_size);
+
+	~MachO();
+};
+
+#endif // MODULE_REGEX_ENABLED
+
+#endif // MACHO_H

+ 570 - 0
platform/osx/export/plist.cpp

@@ -0,0 +1,570 @@
+/*************************************************************************/
+/*  plist.cpp                                                            */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#include "modules/modules_enabled.gen.h" // For regex.
+
+#include "plist.h"
+
+#ifdef MODULE_REGEX_ENABLED
+
+Ref<PListNode> PListNode::new_array() {
+	Ref<PListNode> node = memnew(PListNode());
+	ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>());
+	node->data_type = PList::PLNodeType::PL_NODE_TYPE_ARRAY;
+	return node;
+}
+
+Ref<PListNode> PListNode::new_dict() {
+	Ref<PListNode> node = memnew(PListNode());
+	ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>());
+	node->data_type = PList::PLNodeType::PL_NODE_TYPE_DICT;
+	return node;
+}
+
+Ref<PListNode> PListNode::new_string(const String &p_string) {
+	Ref<PListNode> node = memnew(PListNode());
+	ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>());
+	node->data_type = PList::PLNodeType::PL_NODE_TYPE_STRING;
+	node->data_string = p_string.utf8();
+	return node;
+}
+
+Ref<PListNode> PListNode::new_data(const String &p_string) {
+	Ref<PListNode> node = memnew(PListNode());
+	ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>());
+	node->data_type = PList::PLNodeType::PL_NODE_TYPE_DATA;
+	node->data_string = p_string.utf8();
+	return node;
+}
+
+Ref<PListNode> PListNode::new_date(const String &p_string) {
+	Ref<PListNode> node = memnew(PListNode());
+	ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>());
+	node->data_type = PList::PLNodeType::PL_NODE_TYPE_DATE;
+	node->data_string = p_string.utf8();
+	return node;
+}
+
+Ref<PListNode> PListNode::new_bool(bool p_bool) {
+	Ref<PListNode> node = memnew(PListNode());
+	ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>());
+	node->data_type = PList::PLNodeType::PL_NODE_TYPE_BOOLEAN;
+	node->data_bool = p_bool;
+	return node;
+}
+
+Ref<PListNode> PListNode::new_int(int32_t p_int) {
+	Ref<PListNode> node = memnew(PListNode());
+	ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>());
+	node->data_type = PList::PLNodeType::PL_NODE_TYPE_INTEGER;
+	node->data_int = p_int;
+	return node;
+}
+
+Ref<PListNode> PListNode::new_real(float p_real) {
+	Ref<PListNode> node = memnew(PListNode());
+	ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>());
+	node->data_type = PList::PLNodeType::PL_NODE_TYPE_REAL;
+	node->data_real = p_real;
+	return node;
+}
+
+bool PListNode::push_subnode(const Ref<PListNode> &p_node, const String &p_key) {
+	ERR_FAIL_COND_V(p_node.is_null(), false);
+	if (data_type == PList::PLNodeType::PL_NODE_TYPE_DICT) {
+		ERR_FAIL_COND_V(p_key.is_empty(), false);
+		ERR_FAIL_COND_V(data_dict.has(p_key), false);
+		data_dict[p_key] = p_node;
+		return true;
+	} else if (data_type == PList::PLNodeType::PL_NODE_TYPE_ARRAY) {
+		data_array.push_back(p_node);
+		return true;
+	} else {
+		ERR_FAIL_V_MSG(false, "PList: Invalid parent node type, should be DICT or ARRAY.");
+	}
+}
+
+size_t PListNode::get_asn1_size(uint8_t p_len_octets) const {
+	// Get size of all data, excluding type and size information.
+	switch (data_type) {
+		case PList::PLNodeType::PL_NODE_TYPE_NIL: {
+			return 0;
+		} break;
+		case PList::PLNodeType::PL_NODE_TYPE_DATA:
+		case PList::PLNodeType::PL_NODE_TYPE_DATE: {
+			ERR_FAIL_V_MSG(0, "PList: DATE and DATA nodes are not supported by ASN.1 serialization.");
+		} break;
+		case PList::PLNodeType::PL_NODE_TYPE_STRING: {
+			return data_string.length();
+		} break;
+		case PList::PLNodeType::PL_NODE_TYPE_BOOLEAN: {
+			return 1;
+		} break;
+		case PList::PLNodeType::PL_NODE_TYPE_INTEGER:
+		case PList::PLNodeType::PL_NODE_TYPE_REAL: {
+			return 4;
+		} break;
+		case PList::PLNodeType::PL_NODE_TYPE_ARRAY: {
+			size_t size = 0;
+			for (int i = 0; i < data_array.size(); i++) {
+				size += 1 + _asn1_size_len(p_len_octets) + data_array[i]->get_asn1_size(p_len_octets);
+			}
+			return size;
+		} break;
+		case PList::PLNodeType::PL_NODE_TYPE_DICT: {
+			size_t size = 0;
+			for (const Map<String, Ref<PListNode>>::Element *it = data_dict.front(); it; it = it->next()) {
+				size += 1 + _asn1_size_len(p_len_octets); // Sequence.
+				size += 1 + _asn1_size_len(p_len_octets) + it->key().utf8().length(); //Key.
+				size += 1 + _asn1_size_len(p_len_octets) + it->value()->get_asn1_size(p_len_octets); // Value.
+			}
+			return size;
+		} break;
+		default: {
+			return 0;
+		} break;
+	}
+}
+
+int PListNode::_asn1_size_len(uint8_t p_len_octets) {
+	if (p_len_octets > 1) {
+		return p_len_octets + 1;
+	} else {
+		return 1;
+	}
+}
+
+void PListNode::store_asn1_size(PackedByteArray &p_stream, uint8_t p_len_octets) const {
+	uint32_t size = get_asn1_size(p_len_octets);
+	if (p_len_octets > 1) {
+		p_stream.push_back(0x80 + p_len_octets);
+	}
+	for (int i = p_len_octets - 1; i >= 0; i--) {
+		uint8_t x = (size >> i * 8) & 0xFF;
+		p_stream.push_back(x);
+	}
+}
+
+bool PListNode::store_asn1(PackedByteArray &p_stream, uint8_t p_len_octets) const {
+	// Convert to binary ASN1 stream.
+	bool valid = true;
+	switch (data_type) {
+		case PList::PLNodeType::PL_NODE_TYPE_NIL: {
+			// Nothing to store.
+		} break;
+		case PList::PLNodeType::PL_NODE_TYPE_DATE:
+		case PList::PLNodeType::PL_NODE_TYPE_DATA: {
+			ERR_FAIL_V_MSG(false, "PList: DATE and DATA nodes are not supported by ASN.1 serialization.");
+		} break;
+		case PList::PLNodeType::PL_NODE_TYPE_STRING: {
+			p_stream.push_back(0x0C);
+			store_asn1_size(p_stream, p_len_octets);
+			for (int i = 0; i < data_string.size(); i++) {
+				p_stream.push_back(data_string[i]);
+			}
+		} break;
+		case PList::PLNodeType::PL_NODE_TYPE_BOOLEAN: {
+			p_stream.push_back(0x01);
+			store_asn1_size(p_stream, p_len_octets);
+			if (data_bool) {
+				p_stream.push_back(0x01);
+			} else {
+				p_stream.push_back(0x00);
+			}
+		} break;
+		case PList::PLNodeType::PL_NODE_TYPE_INTEGER: {
+			p_stream.push_back(0x02);
+			store_asn1_size(p_stream, p_len_octets);
+			for (int i = 4; i >= 0; i--) {
+				uint8_t x = (data_int >> i * 8) & 0xFF;
+				p_stream.push_back(x);
+			}
+		} break;
+		case PList::PLNodeType::PL_NODE_TYPE_REAL: {
+			p_stream.push_back(0x03);
+			store_asn1_size(p_stream, p_len_octets);
+			for (int i = 4; i >= 0; i--) {
+				uint8_t x = (data_int >> i * 8) & 0xFF;
+				p_stream.push_back(x);
+			}
+		} break;
+		case PList::PLNodeType::PL_NODE_TYPE_ARRAY: {
+			p_stream.push_back(0x30); // Sequence.
+			store_asn1_size(p_stream, p_len_octets);
+			for (int i = 0; i < data_array.size(); i++) {
+				valid = valid && data_array[i]->store_asn1(p_stream, p_len_octets);
+			}
+		} break;
+		case PList::PLNodeType::PL_NODE_TYPE_DICT: {
+			p_stream.push_back(0x31); // Set.
+			store_asn1_size(p_stream, p_len_octets);
+			for (const Map<String, Ref<PListNode>>::Element *it = data_dict.front(); it; it = it->next()) {
+				CharString cs = it->key().utf8();
+				uint32_t size = cs.length();
+
+				// Sequence.
+				p_stream.push_back(0x30);
+				uint32_t seq_size = 2 * (1 + _asn1_size_len(p_len_octets)) + size + it->value()->get_asn1_size(p_len_octets);
+				if (p_len_octets > 1) {
+					p_stream.push_back(0x80 + p_len_octets);
+				}
+				for (int i = p_len_octets - 1; i >= 0; i--) {
+					uint8_t x = (seq_size >> i * 8) & 0xFF;
+					p_stream.push_back(x);
+				}
+				// Key.
+				p_stream.push_back(0x0C);
+				if (p_len_octets > 1) {
+					p_stream.push_back(0x80 + p_len_octets);
+				}
+				for (int i = p_len_octets - 1; i >= 0; i--) {
+					uint8_t x = (size >> i * 8) & 0xFF;
+					p_stream.push_back(x);
+				}
+				for (uint32_t i = 0; i < size; i++) {
+					p_stream.push_back(cs[i]);
+				}
+				// Value.
+				valid = valid && it->value()->store_asn1(p_stream, p_len_octets);
+			}
+		} break;
+	}
+	return valid;
+}
+
+void PListNode::store_text(String &p_stream, uint8_t p_indent) const {
+	// Convert to text XML stream.
+	switch (data_type) {
+		case PList::PLNodeType::PL_NODE_TYPE_NIL: {
+			// Nothing to store.
+		} break;
+		case PList::PLNodeType::PL_NODE_TYPE_DATA: {
+			p_stream += String("\t").repeat(p_indent);
+			p_stream += "<data>\n";
+			p_stream += String("\t").repeat(p_indent);
+			p_stream += data_string + "\n";
+			p_stream += String("\t").repeat(p_indent);
+			p_stream += "</data>\n";
+		} break;
+		case PList::PLNodeType::PL_NODE_TYPE_DATE: {
+			p_stream += String("\t").repeat(p_indent);
+			p_stream += "<date>";
+			p_stream += data_string;
+			p_stream += "</date>\n";
+		} break;
+		case PList::PLNodeType::PL_NODE_TYPE_STRING: {
+			p_stream += String("\t").repeat(p_indent);
+			p_stream += "<string>";
+			p_stream += String::utf8(data_string);
+			p_stream += "</string>\n";
+		} break;
+		case PList::PLNodeType::PL_NODE_TYPE_BOOLEAN: {
+			p_stream += String("\t").repeat(p_indent);
+			if (data_bool) {
+				p_stream += "<true/>\n";
+			} else {
+				p_stream += "<false/>\n";
+			}
+		} break;
+		case PList::PLNodeType::PL_NODE_TYPE_INTEGER: {
+			p_stream += String("\t").repeat(p_indent);
+			p_stream += "<integer>";
+			p_stream += itos(data_int);
+			p_stream += "</integer>\n";
+		} break;
+		case PList::PLNodeType::PL_NODE_TYPE_REAL: {
+			p_stream += String("\t").repeat(p_indent);
+			p_stream += "<real>";
+			p_stream += rtos(data_real);
+			p_stream += "</real>\n";
+		} break;
+		case PList::PLNodeType::PL_NODE_TYPE_ARRAY: {
+			p_stream += String("\t").repeat(p_indent);
+			p_stream += "<array>\n";
+			for (int i = 0; i < data_array.size(); i++) {
+				data_array[i]->store_text(p_stream, p_indent + 1);
+			}
+			p_stream += String("\t").repeat(p_indent);
+			p_stream += "</array>\n";
+		} break;
+		case PList::PLNodeType::PL_NODE_TYPE_DICT: {
+			p_stream += String("\t").repeat(p_indent);
+			p_stream += "<dict>\n";
+			for (const Map<String, Ref<PListNode>>::Element *it = data_dict.front(); it; it = it->next()) {
+				p_stream += String("\t").repeat(p_indent + 1);
+				p_stream += "<key>";
+				p_stream += it->key();
+				p_stream += "</key>\n";
+				it->value()->store_text(p_stream, p_indent + 1);
+			}
+			p_stream += String("\t").repeat(p_indent);
+			p_stream += "</dict>\n";
+		} break;
+	}
+}
+
+/*************************************************************************/
+
+PList::PList() {
+	root = PListNode::new_dict();
+}
+
+PList::PList(const String &p_string) {
+	load_string(p_string);
+}
+
+bool PList::load_file(const String &p_filename) {
+	root = Ref<PListNode>();
+
+	FileAccessRef fb = FileAccess::open(p_filename, FileAccess::READ);
+	if (!fb) {
+		return false;
+	}
+
+	unsigned char magic[8];
+	fb->get_buffer(magic, 8);
+
+	if (String((const char *)magic, 8) == "bplist00") {
+		ERR_FAIL_V_MSG(false, "PList: Binary property lists are not supported.");
+	} else {
+		// Load text plist.
+		Error err;
+		Vector<uint8_t> array = FileAccess::get_file_as_array(p_filename, &err);
+		ERR_FAIL_COND_V(err != OK, false);
+
+		String ret;
+		ret.parse_utf8((const char *)array.ptr(), array.size());
+		return load_string(ret);
+	}
+}
+
+bool PList::load_string(const String &p_string) {
+	root = Ref<PListNode>();
+
+	int pos = 0;
+	bool in_plist = false;
+	bool done_plist = false;
+	List<Ref<PListNode>> stack;
+	String key;
+	while (pos >= 0) {
+		int open_token_s = p_string.find("<", pos);
+		if (open_token_s == -1) {
+			ERR_FAIL_V_MSG(false, "PList: Unexpected end of data. No tags found.");
+		}
+		int open_token_e = p_string.find(">", open_token_s);
+		pos = open_token_e;
+
+		String token = p_string.substr(open_token_s + 1, open_token_e - open_token_s - 1);
+		if (token.is_empty()) {
+			ERR_FAIL_V_MSG(false, "PList: Invalid token name.");
+		}
+		String value;
+		if (token[0] == '?' || token[0] == '!') { // Skip <?xml ... ?> and <!DOCTYPE ... >
+			int end_token_e = p_string.find(">", open_token_s);
+			pos = end_token_e;
+			continue;
+		}
+
+		if (token.find("plist", 0) == 0) {
+			in_plist = true;
+			continue;
+		}
+
+		if (token == "/plist") {
+			in_plist = false;
+			done_plist = true;
+			break;
+		}
+
+		if (!in_plist) {
+			ERR_FAIL_V_MSG(false, "PList: Node outside of <plist> tag.");
+		}
+
+		if (token == "dict") {
+			if (!stack.is_empty()) {
+				// Add subnode end enter it.
+				Ref<PListNode> dict = PListNode::new_dict();
+				dict->data_type = PList::PLNodeType::PL_NODE_TYPE_DICT;
+				if (!stack.back()->get()->push_subnode(dict, key)) {
+					ERR_FAIL_V_MSG(false, "PList: Can't push subnode, invalid parent type.");
+				}
+				stack.push_back(dict);
+			} else {
+				// Add root node.
+				if (!root.is_null()) {
+					ERR_FAIL_V_MSG(false, "PList: Root node already set.");
+				}
+				Ref<PListNode> dict = PListNode::new_dict();
+				stack.push_back(dict);
+				root = dict;
+			}
+			continue;
+		}
+
+		if (token == "/dict") {
+			// Exit current dict.
+			if (stack.is_empty() || stack.back()->get()->data_type != PList::PLNodeType::PL_NODE_TYPE_DICT) {
+				ERR_FAIL_V_MSG(false, "PList: Mismatched </dict> tag.");
+			}
+			stack.pop_back();
+			continue;
+		}
+
+		if (token == "array") {
+			if (!stack.is_empty()) {
+				// Add subnode end enter it.
+				Ref<PListNode> arr = PListNode::new_array();
+				if (!stack.back()->get()->push_subnode(arr, key)) {
+					ERR_FAIL_V_MSG(false, "PList: Can't push subnode, invalid parent type.");
+				}
+				stack.push_back(arr);
+			} else {
+				// Add root node.
+				if (!root.is_null()) {
+					ERR_FAIL_V_MSG(false, "PList: Root node already set.");
+				}
+				Ref<PListNode> arr = PListNode::new_array();
+				stack.push_back(arr);
+				root = arr;
+			}
+			continue;
+		}
+
+		if (token == "/array") {
+			// Exit current array.
+			if (stack.is_empty() || stack.back()->get()->data_type != PList::PLNodeType::PL_NODE_TYPE_ARRAY) {
+				ERR_FAIL_V_MSG(false, "PList: Mismatched </array> tag.");
+			}
+			stack.pop_back();
+			continue;
+		}
+
+		if (token[token.length() - 1] == '/') {
+			token = token.substr(0, token.length() - 1);
+		} else {
+			int end_token_s = p_string.find("</", pos);
+			if (end_token_s == -1) {
+				ERR_FAIL_V_MSG(false, vformat("PList: Mismatched <%s> tag.", token));
+			}
+			int end_token_e = p_string.find(">", end_token_s);
+			pos = end_token_e;
+			String end_token = p_string.substr(end_token_s + 2, end_token_e - end_token_s - 2);
+			if (end_token != token) {
+				ERR_FAIL_V_MSG(false, vformat("PList: Mismatched <%s> and <%s> token pair.", token, end_token));
+			}
+			value = p_string.substr(open_token_e + 1, end_token_s - open_token_e - 1);
+		}
+		if (token == "key") {
+			key = value;
+		} else {
+			Ref<PListNode> var = nullptr;
+			if (token == "true") {
+				var = PListNode::new_bool(true);
+			} else if (token == "false") {
+				var = PListNode::new_bool(false);
+			} else if (token == "integer") {
+				var = PListNode::new_int(value.to_int());
+			} else if (token == "real") {
+				var = PListNode::new_real(value.to_float());
+			} else if (token == "string") {
+				var = PListNode::new_string(value);
+			} else if (token == "data") {
+				var = PListNode::new_data(value);
+			} else if (token == "date") {
+				var = PListNode::new_date(value);
+			} else {
+				ERR_FAIL_V_MSG(false, "PList: Invalid value type.");
+			}
+			if (stack.is_empty() || !stack.back()->get()->push_subnode(var, key)) {
+				ERR_FAIL_V_MSG(false, "PList: Can't push subnode, invalid parent type.");
+			}
+		}
+	}
+	if (!stack.is_empty() || !done_plist) {
+		ERR_FAIL_V_MSG(false, "PList: Unexpected end of data. Root node is not closed.");
+	}
+	return true;
+}
+
+PackedByteArray PList::save_asn1() const {
+	if (root == nullptr) {
+		ERR_FAIL_V_MSG(PackedByteArray(), "PList: Invalid PList, no root node.");
+	}
+	size_t size = root->get_asn1_size(1);
+	uint8_t len_octets = 0;
+	if (size < 0x80) {
+		len_octets = 1;
+	} else {
+		size = root->get_asn1_size(2);
+		if (size < 0xFFFF) {
+			len_octets = 2;
+		} else {
+			size = root->get_asn1_size(3);
+			if (size < 0xFFFFFF) {
+				len_octets = 3;
+			} else {
+				size = root->get_asn1_size(4);
+				if (size < 0xFFFFFFFF) {
+					len_octets = 4;
+				} else {
+					ERR_FAIL_V_MSG(PackedByteArray(), "PList: Data is too big for ASN.1 serializer, should be < 4 GiB.");
+				}
+			}
+		}
+	}
+
+	PackedByteArray ret;
+	if (!root->store_asn1(ret, len_octets)) {
+		ERR_FAIL_V_MSG(PackedByteArray(), "PList: ASN.1 serializer error.");
+	}
+	return ret;
+}
+
+String PList::save_text() const {
+	if (root == nullptr) {
+		ERR_FAIL_V_MSG(String(), "PList: Invalid PList, no root node.");
+	}
+
+	String ret;
+	ret += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
+	ret += "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n";
+	ret += "<plist version=\"1.0\">\n";
+
+	root->store_text(ret, 0);
+
+	ret += "</plist>\n\n";
+	return ret;
+}
+
+Ref<PListNode> PList::get_root() {
+	return root;
+}
+
+#endif // MODULE_REGEX_ENABLED

+ 116 - 0
platform/osx/export/plist.h

@@ -0,0 +1,116 @@
+/*************************************************************************/
+/*  plist.h                                                              */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+// Property list file format (application/x-plist) parser, property list ASN-1 serialization.
+
+#ifndef PLIST_H
+#define PLIST_H
+
+#include "core/crypto/crypto_core.h"
+#include "core/io/file_access.h"
+#include "modules/modules_enabled.gen.h" // For regex.
+
+#ifdef MODULE_REGEX_ENABLED
+
+class PListNode;
+
+class PList : public RefCounted {
+	friend class PListNode;
+
+public:
+	enum PLNodeType {
+		PL_NODE_TYPE_NIL,
+		PL_NODE_TYPE_STRING,
+		PL_NODE_TYPE_ARRAY,
+		PL_NODE_TYPE_DICT,
+		PL_NODE_TYPE_BOOLEAN,
+		PL_NODE_TYPE_INTEGER,
+		PL_NODE_TYPE_REAL,
+		PL_NODE_TYPE_DATA,
+		PL_NODE_TYPE_DATE,
+	};
+
+private:
+	Ref<PListNode> root;
+
+public:
+	PList();
+	PList(const String &p_string);
+
+	bool load_file(const String &p_filename);
+	bool load_string(const String &p_string);
+
+	PackedByteArray save_asn1() const;
+	String save_text() const;
+
+	Ref<PListNode> get_root();
+};
+
+/*************************************************************************/
+
+class PListNode : public RefCounted {
+	static int _asn1_size_len(uint8_t p_len_octets);
+
+public:
+	PList::PLNodeType data_type = PList::PLNodeType::PL_NODE_TYPE_NIL;
+
+	CharString data_string;
+	Vector<Ref<PListNode>> data_array;
+	Map<String, Ref<PListNode>> data_dict;
+	union {
+		int32_t data_int;
+		bool data_bool;
+		float data_real;
+	};
+
+	static Ref<PListNode> new_array();
+	static Ref<PListNode> new_dict();
+	static Ref<PListNode> new_string(const String &p_string);
+	static Ref<PListNode> new_data(const String &p_string);
+	static Ref<PListNode> new_date(const String &p_string);
+	static Ref<PListNode> new_bool(bool p_bool);
+	static Ref<PListNode> new_int(int32_t p_int);
+	static Ref<PListNode> new_real(float p_real);
+
+	bool push_subnode(const Ref<PListNode> &p_node, const String &p_key = "");
+
+	size_t get_asn1_size(uint8_t p_len_octets) const;
+
+	void store_asn1_size(PackedByteArray &p_stream, uint8_t p_len_octets) const;
+	bool store_asn1(PackedByteArray &p_stream, uint8_t p_len_octets) const;
+	void store_text(String &p_stream, uint8_t p_indent) const;
+
+	PListNode() {}
+	~PListNode() {}
+};
+
+#endif // MODULE_REGEX_ENABLED
+
+#endif // PLIST_H