virtualx-engine/platform/macos/export/codesign.cpp
2024-06-12 22:26:41 +02:00

1573 lines
53 KiB
C++

/**************************************************************************/
/* codesign.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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 "core/io/plist.h"
#include "core/os/os.h"
#include "editor/editor_paths.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) {
Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::READ);
ERR_FAIL_COND_V_MSG(fa.is_null(), 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);
return CryptoCore::b64_encode_str(hash, 0x14);
}
String CodeSignCodeResources::hash_sha256_base64(const String &p_path) {
Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::READ);
ERR_FAIL_COND_V_MSG(fa.is_null(), 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);
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.path_join(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.path_join(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.path_join(p_path));
f.hash2 = hash_sha256_base64(p_root.path_join(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]); \
} \
}
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
ERR_FAIL_COND_V(da.is_null(), false);
Vector<String> files_to_add;
if (LipO::is_lipo(p_exepath)) {
String tmp_path_name = EditorPaths::get_singleton()->get_cache_dir().path_join("_lipo");
Error err = da->make_dir_recursive(tmp_path_name);
ERR_FAIL_COND_V_MSG(err != OK, 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.path_join("_rqexe_" + itos(i)))) {
CLEANUP();
ERR_FAIL_V_MSG(false, "CodeSign/CodeResources: Failed to extract thin binary.");
}
files_to_add.push_back(tmp_path_name.path_join("_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) {
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
ERR_FAIL_COND_V(da.is_null(), false);
Error err = da->change_dir(p_root.path_join(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.path_join(p_path).path_join(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.path_join(n));
String fmw_ver = "Current"; // Framework version (default).
String info_path;
String main_exe;
bool bundle = false;
if (da->file_exists(path.path_join("Contents/Info.plist"))) {
info_path = path.path_join("Contents/Info.plist");
main_exe = path.path_join("Contents/MacOS");
bundle = true;
} else if (da->file_exists(path.path_join(vformat("Versions/%s/Resources/Info.plist", fmw_ver)))) {
info_path = path.path_join(vformat("Versions/%s/Resources/Info.plist", fmw_ver));
main_exe = path.path_join(vformat("Versions/%s", fmw_ver));
bundle = true;
} else if (da->file_exists(path.path_join("Resources/Info.plist"))) {
info_path = path.path_join("Resources/Info.plist");
main_exe = path;
bundle = true;
} else if (da->file_exists(path.path_join("Info.plist"))) {
info_path = path.path_join("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.path_join(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.path_join(n), main_exe);
} else {
ret = ret && add_folder_recursive(p_root, p_path.path_join(n), p_main_exe_path);
}
} else {
ret = ret && add_file1(p_root, p_path.path_join(n));
ret = ret && add_file2(p_root, p_path.path_join(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.");
Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::WRITE);
ERR_FAIL_COND_V_MSG(fa.is_null(), 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());
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(Ref<FileAccess> p_file) const {
ERR_FAIL_COND_MSG(p_file.is_null(), "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(Ref<FileAccess> p_file) const {
ERR_FAIL_COND_MSG(p_file.is_null(), "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(Ref<FileAccess> p_file) const {
ERR_FAIL_COND_MSG(p_file.is_null(), "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 = reinterpret_cast<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(Ref<FileAccess> p_file) const {
ERR_FAIL_COND_MSG(p_file.is_null(), "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(Ref<FileAccess> p_file) const {
ERR_FAIL_COND_MSG(p_file.is_null(), "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(Ref<FileAccess> p_file) const {
ERR_FAIL_COND_MSG(p_file.is_null(), "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;
Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ);
ERR_FAIL_COND_V_MSG(f.is_null(), 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;
Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ);
ERR_FAIL_COND_V_MSG(f.is_null(), 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;
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
if (da.is_null()) {
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("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.path_join(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().path_join("_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.path_join("_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.path_join("_exe_" + itos(i)));
}
}
} else if (MachO::is_macho(main_exe)) {
print_verbose("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("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.path_join("_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.path_join("_CodeSignature").path_join("CodeResources"));
res_hash1 = file_hash_sha1(p_bundle_path.path_join("_CodeSignature").path_join("CodeResources"));
res_hash2 = file_hash_sha256(p_bundle_path.path_join("_CodeSignature").path_join("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()) {
CryptoCore::RandomGenerator rng;
ERR_FAIL_COND_V_MSG(rng.init(), FAILED, "Failed to initialize random number generator.");
uint8_t uuid[16];
Error err = rng.get_random_bytes(uuid, 16);
ERR_FAIL_COND_V_MSG(err, err, "Failed to generate UUID.");
id = (String("a-55554944") /*a-UUID*/ + String::hex_encode_buffer(uuid, 16));
}
CharString uuid_str = id.utf8();
print_verbose(vformat("CodeSign: Used bundle ID: %s", id));
print_verbose("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("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("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("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("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("CodeSign: Generating signature...");
Ref<CodeSignSignature> cs;
cs = Ref<CodeSignSignature>(memnew(CodeSignSignature()));
print_verbose("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("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) {
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
if (da.is_null()) {
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.path_join("Contents/Info.plist"))) {
info_path = p_path.path_join("Contents/Info.plist");
main_exe = p_path.path_join("Contents/MacOS");
bundle_path = p_path.path_join("Contents");
bundle = true;
} else if (da->file_exists(p_path.path_join(vformat("Versions/%s/Resources/Info.plist", fmw_ver)))) {
info_path = p_path.path_join(vformat("Versions/%s/Resources/Info.plist", fmw_ver));
main_exe = p_path.path_join(vformat("Versions/%s", fmw_ver));
bundle_path = p_path.path_join(vformat("Versions/%s", fmw_ver));
bundle = true;
} else if (da->file_exists(p_path.path_join("Resources/Info.plist"))) {
info_path = p_path.path_join("Resources/Info.plist");
main_exe = p_path;
bundle_path = p_path;
bundle = true;
} else if (da->file_exists(p_path.path_join("Info.plist"))) {
info_path = p_path.path_join("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