Skip to content

Commit 2a4d480

Browse files
committed
[Editor] Add support for editor template integrity and signature verification.
1 parent 06faefc commit 2a4d480

File tree

5 files changed

+317
-19
lines changed

5 files changed

+317
-19
lines changed

editor/export/SCsub

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,14 @@ from misc.utility.scons_hints import *
33

44
Import("env")
55

6+
import editor_export_builders
7+
8+
# Template keys
9+
flist = Glob("#editor/export/keys/*.pub")
10+
env.CommandNoCache(
11+
"#editor/export/export_template_keys.gen.h",
12+
flist,
13+
env.Run(editor_export_builders.make_keys_header),
14+
)
15+
616
env.add_source_files(env.editor_sources, "*.cpp")
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Functions used to generate source files during build time"""
2+
3+
import methods
4+
5+
6+
def make_keys_header(target, source, env):
7+
with methods.generated_wrapper(str(target[0])) as file:
8+
file.write("inline constexpr const char *trusted_public_keys[] = {")
9+
for src in map(str, source):
10+
with open(src, encoding="utf-8", newline="\n") as src_file:
11+
file.write(f"""\
12+
{methods.to_raw_cstring(src_file.read())},
13+
""")
14+
file.write("};")

editor/export/export_template_manager.cpp

Lines changed: 263 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,16 @@
3030

3131
#include "export_template_manager.h"
3232

33+
#include "core/crypto/crypto.h"
34+
#include "core/crypto/crypto_core.h"
3335
#include "core/io/dir_access.h"
3436
#include "core/io/json.h"
3537
#include "core/io/zip_io.h"
3638
#include "core/version.h"
3739
#include "editor/editor_node.h"
3840
#include "editor/editor_string_names.h"
3941
#include "editor/export/editor_export_preset.h"
42+
#include "editor/export/export_template_keys.gen.h"
4043
#include "editor/file_system/editor_file_system.h"
4144
#include "editor/file_system/editor_paths.h"
4245
#include "editor/gui/progress_dialog.h"
@@ -47,6 +50,7 @@
4750
#include "scene/gui/link_button.h"
4851
#include "scene/gui/menu_button.h"
4952
#include "scene/gui/option_button.h"
53+
#include "scene/gui/rich_text_label.h"
5054
#include "scene/gui/separator.h"
5155
#include "scene/gui/tree.h"
5256
#include "scene/main/http_request.h"
@@ -239,17 +243,7 @@ void ExportTemplateManager::_download_template_completed(int p_status, int p_cod
239243
String path = download_templates->get_download_file();
240244

241245
is_downloading_templates = false;
242-
bool ret = _install_file_selected(path, true);
243-
if (ret) {
244-
// Clean up downloaded file.
245-
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
246-
Error err = da->remove(path);
247-
if (err != OK) {
248-
EditorNode::get_singleton()->add_io_error(TTR("Cannot remove temporary file:") + "\n" + path + "\n");
249-
}
250-
} else {
251-
EditorNode::get_singleton()->add_io_error(vformat(TTR("Templates installation failed.\nThe problematic templates archives can be found at '%s'."), path));
252-
}
246+
_verify_and_install_file_selected(path, true, true);
253247
}
254248
} break;
255249
}
@@ -426,14 +420,219 @@ void ExportTemplateManager::_install_file() {
426420
install_file_dialog->popup_file_dialog();
427421
}
428422

429-
bool ExportTemplateManager::_install_file_selected(const String &p_file, bool p_skip_progress) {
423+
static Vector<uint8_t> _get_current_file_hash(unz_file_info &p_info, unzFile p_pkg) {
424+
CryptoCore::SHA256Context ctx;
425+
Error err = ctx.start();
426+
ERR_FAIL_COND_V(err != OK, Vector<uint8_t>());
427+
428+
unzOpenCurrentFile(p_pkg);
429+
uint8_t buf[4096];
430+
int64_t to_read = p_info.uncompressed_size;
431+
while (to_read) {
432+
int64_t len = MIN(to_read, 4096);
433+
int64_t read = unzReadCurrentFile(p_pkg, buf, len);
434+
ERR_FAIL_COND_V(read < 0, Vector<uint8_t>());
435+
err = ctx.update((const uint8_t *)buf, read);
436+
ERR_FAIL_COND_V(err != OK, Vector<uint8_t>());
437+
to_read -= read;
438+
if (read == UNZ_EOF) {
439+
break;
440+
}
441+
}
442+
unzCloseCurrentFile(p_pkg);
443+
444+
Vector<uint8_t> out;
445+
out.resize(32);
446+
err = ctx.finish((unsigned char *)out.ptrw());
447+
ERR_FAIL_COND_V(err != OK, Vector<uint8_t>());
448+
449+
return out;
450+
}
451+
452+
void ExportTemplateManager::_clenup(const String &p_file, bool p_remove_file) {
453+
if (p_file.is_empty()) {
454+
return;
455+
}
456+
457+
if (p_remove_file) {
458+
// Clean up downloaded file.
459+
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
460+
Error err = da->remove(p_file);
461+
if (err != OK) {
462+
EditorNode::get_singleton()->add_io_error(TTR("Cannot remove temporary file:") + "\n" + p_file + "\n");
463+
}
464+
}
465+
}
466+
467+
void ExportTemplateManager::_verify_and_install_file_selected(const String &p_file, bool p_skip_progress, bool p_remove_file) {
468+
HashMap<String, Vector<uint8_t>> expected_hashes;
469+
HashMap<String, Vector<uint8_t>> file_hashes;
470+
Vector<uint8_t> signature;
471+
Vector<uint8_t> manifest_hash;
472+
Vector<String> user_keys = EDITOR_GET("export/template_trusted_public_keys");
473+
bool has_keys = std::size(trusted_public_keys) > 0 || !user_keys.is_empty();
474+
430475
Ref<FileAccess> io_fa;
431476
zlib_filefunc_def io = zipio_create_io(&io_fa);
432477

433478
unzFile pkg = unzOpen2(p_file.utf8().get_data(), &io);
434479
if (!pkg) {
435480
EditorNode::get_singleton()->show_warning(TTR("Can't open the export templates file."));
436-
return false;
481+
_clenup(p_file, p_remove_file);
482+
return;
483+
}
484+
485+
int ret = unzGoToFirstFile(pkg);
486+
while (ret == UNZ_OK) {
487+
unz_file_info info;
488+
char fname[16384];
489+
ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
490+
if (ret != UNZ_OK) {
491+
break;
492+
}
493+
String file = String::utf8(fname);
494+
if (file == ".manifest") {
495+
Vector<uint8_t> uncomp_data;
496+
uncomp_data.resize(info.uncompressed_size);
497+
498+
unzOpenCurrentFile(pkg);
499+
ret = unzReadCurrentFile(pkg, uncomp_data.ptrw(), uncomp_data.size());
500+
ERR_BREAK(ret < 0);
501+
unzCloseCurrentFile(pkg);
502+
Vector<String> manifest = String::utf8((const char *)uncomp_data.ptr(), uncomp_data.size()).split("\n");
503+
for (const String &file_info : manifest) {
504+
String hash = file_info.get_slice(" ", 0);
505+
String name = file_info.get_slice(" ", 1).trim_prefix("*");
506+
if (!name.is_empty() && !hash.is_empty() && name != ".manifest" && name != ".signature") {
507+
expected_hashes[name] = hash.hex_decode();
508+
}
509+
}
510+
manifest_hash = _get_current_file_hash(info, pkg);
511+
} else if (file == ".signature") {
512+
signature.resize(info.uncompressed_size);
513+
unzOpenCurrentFile(pkg);
514+
ret = unzReadCurrentFile(pkg, signature.ptrw(), signature.size());
515+
ERR_BREAK(ret < 0);
516+
unzCloseCurrentFile(pkg);
517+
} else {
518+
file_hashes[file] = _get_current_file_hash(info, pkg);
519+
}
520+
521+
ret = unzGoToNextFile(pkg);
522+
}
523+
unzClose(pkg);
524+
525+
// Unsigned export templates file.
526+
if (signature.is_empty() && expected_hashes.is_empty()) {
527+
if (!has_keys) {
528+
// No trusted keys specified, install without user interaction.
529+
_install_file_selected(p_file, p_skip_progress);
530+
_clenup(p_file, p_remove_file);
531+
} else {
532+
// Show confirmation dialog.
533+
ver_file = p_file;
534+
ver_skip_progress = p_skip_progress;
535+
ver_remove_file = p_remove_file;
536+
verification_dialog_accept->popup_centered();
537+
}
538+
return;
539+
}
540+
541+
Vector<String> error_msgs;
542+
const String &bullet = U"";
543+
544+
// Verify file hashes.
545+
bool files_ok = true;
546+
for (const KeyValue<String, Vector<uint8_t>> &E : file_hashes) {
547+
if (!expected_hashes.has(E.key)) {
548+
error_msgs.push_back(bullet + vformat(TTR("Unexpected file \"%s\" found."), E.key));
549+
files_ok = false;
550+
} else {
551+
if (E.value != expected_hashes[E.key]) {
552+
error_msgs.push_back(bullet + vformat(TTR("File \"%s\" hash mismatch."), E.key));
553+
files_ok = false;
554+
}
555+
}
556+
}
557+
for (const KeyValue<String, Vector<uint8_t>> &E : expected_hashes) {
558+
if (!file_hashes.has(E.key)) {
559+
error_msgs.push_back(bullet + vformat(TTR("Missing file \"%s\"."), E.key));
560+
files_ok = false;
561+
}
562+
}
563+
564+
bool signature_ok = false;
565+
if (!has_keys) {
566+
error_msgs.push_back(bullet + TTR("Signature verification skipped, no trusted public keys configured."));
567+
signature_ok = true;
568+
}
569+
if (!signature_ok) {
570+
for (size_t i = 0; i < std::size(trusted_public_keys); i++) {
571+
Ref<CryptoKey> key = Ref<CryptoKey>(CryptoKey::create());
572+
ERR_FAIL_COND(key.is_null());
573+
if (key->load_from_string(trusted_public_keys[i], true) != OK) {
574+
continue;
575+
}
576+
577+
Ref<Crypto> crypto = Crypto::create();
578+
ERR_FAIL_COND(crypto.is_null());
579+
if (crypto->verify(HashingContext::HASH_SHA256, manifest_hash, signature, key)) {
580+
signature_ok = true;
581+
break;
582+
}
583+
}
584+
}
585+
if (!signature_ok) {
586+
for (const String &k : user_keys) {
587+
String ukey = vformat("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", k);
588+
Ref<CryptoKey> key = Ref<CryptoKey>(CryptoKey::create());
589+
ERR_FAIL_COND(key.is_null());
590+
if (key->load_from_string(ukey, true) != OK) {
591+
continue;
592+
}
593+
594+
Ref<Crypto> crypto = Crypto::create();
595+
ERR_FAIL_COND(crypto.is_null());
596+
if (crypto->verify(HashingContext::HASH_SHA256, manifest_hash, signature, key)) {
597+
signature_ok = true;
598+
break;
599+
}
600+
}
601+
}
602+
if (!signature_ok) {
603+
error_msgs.push_back(bullet + TTR("Signature verification failed."));
604+
}
605+
606+
if (!files_ok || !signature_ok) {
607+
verification_fail_message_label->set_text(String("\n").join(error_msgs));
608+
verification_dialog_fail->popup_centered();
609+
_clenup(p_file, p_remove_file);
610+
return;
611+
}
612+
613+
_install_file_selected(p_file, p_skip_progress);
614+
_clenup(p_file, p_remove_file);
615+
}
616+
617+
void ExportTemplateManager::_install_continue() {
618+
_install_file_selected(ver_file, ver_skip_progress);
619+
_clenup(ver_file, ver_remove_file);
620+
ver_file = String();
621+
}
622+
623+
void ExportTemplateManager::_install_cancel() {
624+
_clenup(ver_file, ver_remove_file);
625+
ver_file = String();
626+
}
627+
628+
void ExportTemplateManager::_install_file_selected(const String &p_file, bool p_skip_progress) {
629+
Ref<FileAccess> io_fa;
630+
zlib_filefunc_def io = zipio_create_io(&io_fa);
631+
632+
unzFile pkg = unzOpen2(p_file.utf8().get_data(), &io);
633+
if (!pkg) {
634+
EditorNode::get_singleton()->show_warning(TTR("Can't open the export templates file."));
635+
return;
437636
}
438637
int ret = unzGoToFirstFile(pkg);
439638

@@ -476,7 +675,7 @@ bool ExportTemplateManager::_install_file_selected(const String &p_file, bool p_
476675
if (data_str.get_slice_count(".") < 3) {
477676
EditorNode::get_singleton()->show_warning(vformat(TTR("Invalid version.txt format inside the export templates file: %s."), data_str));
478677
unzClose(pkg);
479-
return false;
678+
return;
480679
}
481680

482681
version = data_str;
@@ -493,7 +692,7 @@ bool ExportTemplateManager::_install_file_selected(const String &p_file, bool p_
493692
if (version.is_empty()) {
494693
EditorNode::get_singleton()->show_warning(TTR("No version.txt found inside the export templates file."));
495694
unzClose(pkg);
496-
return false;
695+
return;
497696
}
498697

499698
Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
@@ -502,7 +701,7 @@ bool ExportTemplateManager::_install_file_selected(const String &p_file, bool p_
502701
if (err != OK) {
503702
EditorNode::get_singleton()->show_warning(TTR("Error creating path for extracting templates:") + "\n" + template_path);
504703
unzClose(pkg);
505-
return false;
704+
return;
506705
}
507706

508707
EditorProgress *p = nullptr;
@@ -594,7 +793,6 @@ bool ExportTemplateManager::_install_file_selected(const String &p_file, bool p_
594793

595794
_update_template_status();
596795
EditorSettings::get_singleton()->set("_export_template_download_directory", p_file.get_base_dir());
597-
return true;
598796
}
599797

600798
void ExportTemplateManager::_uninstall_template(const String &p_version) {
@@ -979,6 +1177,8 @@ ExportTemplateManager::ExportTemplateManager() {
9791177
set_hide_on_ok(false);
9801178
set_ok_button_text(TTR("Close"));
9811179

1180+
EDITOR_DEF_BASIC("export/template_trusted_public_keys", Vector<String>());
1181+
9821182
VBoxContainer *main_vb = memnew(VBoxContainer);
9831183
add_child(main_vb);
9841184

@@ -1162,11 +1362,56 @@ ExportTemplateManager::ExportTemplateManager() {
11621362
install_file_dialog->set_file_mode(FileDialog::FILE_MODE_OPEN_FILE);
11631363
install_file_dialog->set_current_dir(EDITOR_DEF("_export_template_download_directory", ""));
11641364
install_file_dialog->add_filter("*.tpz", TTR("Godot Export Templates"));
1165-
install_file_dialog->connect("file_selected", callable_mp(this, &ExportTemplateManager::_install_file_selected).bind(false));
1365+
install_file_dialog->connect("file_selected", callable_mp(this, &ExportTemplateManager::_verify_and_install_file_selected).bind(false, false), CONNECT_DEFERRED);
11661366
add_child(install_file_dialog);
11671367

11681368
hide_dialog_accept = memnew(AcceptDialog);
11691369
hide_dialog_accept->set_text(TTR("The templates will continue to download.\nYou may experience a short editor freeze when they finish."));
11701370
add_child(hide_dialog_accept);
11711371
hide_dialog_accept->connect(SceneStringName(confirmed), callable_mp(this, &ExportTemplateManager::_hide_dialog));
1372+
1373+
verification_dialog_accept = memnew(AcceptDialog);
1374+
verification_dialog_accept->add_cancel_button();
1375+
verification_dialog_accept->set_title(TTR("Unsigned Export Templates File"));
1376+
verification_dialog_accept->set_ok_button_text(TTR("Install Anyway"));
1377+
add_child(verification_dialog_accept);
1378+
verification_dialog_accept->connect(SceneStringName(confirmed), callable_mp(this, &ExportTemplateManager::_install_continue));
1379+
verification_dialog_accept->connect(SNAME("canceled"), callable_mp(this, &ExportTemplateManager::_install_cancel));
1380+
1381+
VBoxContainer *verification_accept_message_vbox = memnew(VBoxContainer);
1382+
verification_dialog_accept->add_child(verification_accept_message_vbox);
1383+
1384+
verification_accept_message_title = memnew(Label);
1385+
verification_accept_message_title->set_text(TTR("Can't validate the export templates file, templates file is not signed."));
1386+
verification_accept_message_title->set_v_size_flags(Control::SIZE_SHRINK_BEGIN);
1387+
verification_accept_message_title->set_theme_type_variation("HeaderSmall");
1388+
verification_accept_message_vbox->add_child(verification_accept_message_title);
1389+
1390+
verification_accept_message_vbox->add_spacer();
1391+
1392+
Label *verification_accept_message_label = memnew(Label);
1393+
verification_accept_message_label->set_v_size_flags(Control::SIZE_SHRINK_BEGIN);
1394+
verification_accept_message_label->set_text(TTR("Installing this export templates file might put your computer at risk."));
1395+
verification_accept_message_vbox->add_child(verification_accept_message_label);
1396+
1397+
verification_dialog_fail = memnew(AcceptDialog);
1398+
verification_dialog_fail->set_title(TTR("Export Templates File Verification Failed"));
1399+
add_child(verification_dialog_fail);
1400+
1401+
VBoxContainer *verification_fail_message_vbox = memnew(VBoxContainer);
1402+
verification_dialog_fail->add_child(verification_fail_message_vbox);
1403+
1404+
verification_fail_message_title = memnew(Label);
1405+
verification_fail_message_title->set_text(TTR("Can't install the export templates file, templates file damaged or has invalid signature."));
1406+
verification_fail_message_title->set_v_size_flags(Control::SIZE_SHRINK_BEGIN);
1407+
verification_fail_message_title->set_theme_type_variation("HeaderSmall");
1408+
verification_fail_message_vbox->add_child(verification_fail_message_title);
1409+
1410+
verification_fail_message_vbox->add_spacer();
1411+
1412+
verification_fail_message_label = memnew(RichTextLabel);
1413+
verification_fail_message_label->set_focus_mode(Control::FOCUS_ACCESSIBILITY);
1414+
verification_fail_message_label->set_v_size_flags(Control::SIZE_EXPAND_FILL);
1415+
verification_fail_message_label->set_custom_minimum_size(Size2(0, 200) * EditorScale::get_scale());
1416+
verification_fail_message_vbox->add_child(verification_fail_message_label);
11721417
}

0 commit comments

Comments
 (0)