[TLS] Add support for platform-specific CA bundles.

Adds a new OS::get_system_ca_certs method which can be implemented by
platforms to retrieve the list of trusted CA certificates using OS
specific APIs.

The function should return the certificates in PEM format, and is
currently implemented for Windows/macOS/LinuxBSD(*)/Android.

mbedTLS will fall back to bundled certificates when the OS returns no
certificates.

(*) LinuxBSD does not have a standardized certificates store location.
    The current implementation will test for common locations and may
    return an empty string on some distributions (falling back to the
    bundled certificates).
This commit is contained in:
Fabio Alessandrelli 2023-04-13 21:17:55 +02:00
parent 4e1d5be9d3
commit 6fd9982358
17 changed files with 180 additions and 13 deletions

View File

@ -217,7 +217,11 @@ opts.Add(BoolVariable("disable_advanced_gui", "Disable advanced GUI nodes and be
opts.Add("build_profile", "Path to a file containing a feature build profile", "") opts.Add("build_profile", "Path to a file containing a feature build profile", "")
opts.Add(BoolVariable("modules_enabled_by_default", "If no, disable all modules except ones explicitly enabled", True)) opts.Add(BoolVariable("modules_enabled_by_default", "If no, disable all modules except ones explicitly enabled", True))
opts.Add(BoolVariable("no_editor_splash", "Don't use the custom splash screen for the editor", True)) opts.Add(BoolVariable("no_editor_splash", "Don't use the custom splash screen for the editor", True))
opts.Add("system_certs_path", "Use this path as SSL certificates default for editor (for package maintainers)", "") opts.Add(
"system_certs_path",
"Use this path as TLS certificates default for editor and Linux/BSD export templates (for package maintainers)",
"",
)
opts.Add(BoolVariable("use_precise_math_checks", "Math checks use very precise epsilon (debug option)", False)) opts.Add(BoolVariable("use_precise_math_checks", "Math checks use very precise epsilon (debug option)", False))
# Thirdparty libraries # Thirdparty libraries

View File

@ -137,6 +137,7 @@ public:
virtual String get_stdin_string() = 0; virtual String get_stdin_string() = 0;
virtual Error get_entropy(uint8_t *r_buffer, int p_bytes) = 0; // Should return cryptographically-safe random bytes. virtual Error get_entropy(uint8_t *r_buffer, int p_bytes) = 0; // Should return cryptographically-safe random bytes.
virtual String get_system_ca_certificates() { return ""; } // Concatenated certificates in PEM format.
virtual PackedStringArray get_connected_midi_inputs(); virtual PackedStringArray get_connected_midi_inputs();
virtual void open_midi_inputs(); virtual void open_midi_inputs();

View File

@ -36,6 +36,7 @@
#include "core/config/project_settings.h" #include "core/config/project_settings.h"
#include "core/io/certs_compressed.gen.h" #include "core/io/certs_compressed.gen.h"
#include "core/io/compression.h" #include "core/io/compression.h"
#include "core/os/os.h"
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
#include "editor/editor_settings.h" #include "editor/editor_settings.h"
@ -337,20 +338,26 @@ void CryptoMbedTLS::load_default_certificates(String p_path) {
if (!p_path.is_empty()) { if (!p_path.is_empty()) {
// Use certs defined in project settings. // Use certs defined in project settings.
default_certs->load(p_path); default_certs->load(p_path);
} } else {
// Try to use system certs otherwise.
String system_certs = OS::get_singleton()->get_system_ca_certificates();
if (!system_certs.is_empty()) {
CharString cs = system_certs.utf8();
default_certs->load_from_memory((const uint8_t *)cs.get_data(), cs.size());
print_verbose("Loaded system CA certificates");
}
#ifdef BUILTIN_CERTS_ENABLED #ifdef BUILTIN_CERTS_ENABLED
else { else {
// Use builtin certs only if user did not override it in project settings. // Use builtin certs if there are no system certs.
PackedByteArray out; PackedByteArray certs;
out.resize(_certs_uncompressed_size + 1); certs.resize(_certs_uncompressed_size + 1);
Compression::decompress(out.ptrw(), _certs_uncompressed_size, _certs_compressed, _certs_compressed_size, Compression::MODE_DEFLATE); Compression::decompress(certs.ptrw(), _certs_uncompressed_size, _certs_compressed, _certs_compressed_size, Compression::MODE_DEFLATE);
out.write[_certs_uncompressed_size] = 0; // Make sure it ends with string terminator certs.write[_certs_uncompressed_size] = 0; // Make sure it ends with string terminator
#ifdef DEBUG_ENABLED default_certs->load_from_memory(certs.ptr(), certs.size());
print_verbose("Loaded builtin certs"); print_verbose("Loaded builtin CA certificates");
}
#endif #endif
default_certs->load_from_memory(out.ptr(), out.size());
} }
#endif
} }
Ref<CryptoKey> CryptoMbedTLS::generate_rsa(int p_bytes) { Ref<CryptoKey> CryptoMbedTLS::generate_rsa(int p_bytes) {

View File

@ -1044,6 +1044,11 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
return PermissionsUtil.getGrantedPermissions(getActivity()); return PermissionsUtil.getGrantedPermissions(getActivity());
} }
@Keep
private String getCACertificates() {
return GodotNetUtils.getCACertificates();
}
/** /**
* The download state should trigger changes in the UI --- it may be useful * The download state should trigger changes in the UI --- it may be useful
* to show the state as being indeterminate at times. This sample can be * to show the state as being indeterminate at times. This sample can be

View File

@ -33,11 +33,17 @@ package org.godotengine.godot.utils;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.net.wifi.WifiManager; import android.net.wifi.WifiManager;
import android.util.Base64;
import android.util.Log; import android.util.Log;
import java.io.StringWriter;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
/** /**
* This class handles Android-specific networking functions. * This class handles Android-specific networking functions.
* For now, it only provides access to WifiManager.MulticastLock, which is needed on some devices * It provides access to the CA certificates KeyStore, and the WifiManager.MulticastLock, which is needed on some devices
* to receive broadcast and multicast packets. * to receive broadcast and multicast packets.
*/ */
public class GodotNetUtils { public class GodotNetUtils {
@ -79,4 +85,34 @@ public class GodotNetUtils {
Log.e("Godot", "Exception during multicast lock release: " + e); Log.e("Godot", "Exception during multicast lock release: " + e);
} }
} }
/**
* Retrieves the list of trusted CA certificates from the "AndroidCAStore" and returns them in PRM format.
* @see https://developer.android.com/reference/java/security/KeyStore .
* @return A string of concatenated X509 certificates in PEM format.
*/
public static String getCACertificates() {
try {
KeyStore ks = KeyStore.getInstance("AndroidCAStore");
StringBuilder writer = new StringBuilder();
if (ks != null) {
ks.load(null, null);
Enumeration<String> aliases = ks.aliases();
while (aliases.hasMoreElements()) {
String alias = (String)aliases.nextElement();
X509Certificate cert = (X509Certificate)ks.getCertificate(alias);
writer.append("-----BEGIN CERTIFICATE-----\n");
writer.append(Base64.encodeToString(cert.getEncoded(), Base64.DEFAULT));
writer.append("-----END CERTIFICATE-----\n");
}
}
return writer.toString();
} catch (Exception e) {
Log.e("Godot", "Exception while reading CA certificates: " + e);
return "";
}
}
} }

View File

@ -70,6 +70,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
_request_permission = p_env->GetMethodID(godot_class, "requestPermission", "(Ljava/lang/String;)Z"); _request_permission = p_env->GetMethodID(godot_class, "requestPermission", "(Ljava/lang/String;)Z");
_request_permissions = p_env->GetMethodID(godot_class, "requestPermissions", "()Z"); _request_permissions = p_env->GetMethodID(godot_class, "requestPermissions", "()Z");
_get_granted_permissions = p_env->GetMethodID(godot_class, "getGrantedPermissions", "()[Ljava/lang/String;"); _get_granted_permissions = p_env->GetMethodID(godot_class, "getGrantedPermissions", "()[Ljava/lang/String;");
_get_ca_certificates = p_env->GetMethodID(godot_class, "getCACertificates", "()Ljava/lang/String;");
_init_input_devices = p_env->GetMethodID(godot_class, "initInputDevices", "()V"); _init_input_devices = p_env->GetMethodID(godot_class, "initInputDevices", "()V");
_get_surface = p_env->GetMethodID(godot_class, "getSurface", "()Landroid/view/Surface;"); _get_surface = p_env->GetMethodID(godot_class, "getSurface", "()Landroid/view/Surface;");
_is_activity_resumed = p_env->GetMethodID(godot_class, "isActivityResumed", "()Z"); _is_activity_resumed = p_env->GetMethodID(godot_class, "isActivityResumed", "()Z");
@ -310,6 +311,17 @@ Vector<String> GodotJavaWrapper::get_granted_permissions() const {
return permissions_list; return permissions_list;
} }
String GodotJavaWrapper::get_ca_certificates() const {
if (_get_ca_certificates) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL_V(env, String());
jstring s = (jstring)env->CallObjectMethod(godot_instance, _get_ca_certificates);
return jstring_to_string(s, env);
} else {
return String();
}
}
void GodotJavaWrapper::init_input_devices() { void GodotJavaWrapper::init_input_devices() {
if (_init_input_devices) { if (_init_input_devices) {
JNIEnv *env = get_jni_env(); JNIEnv *env = get_jni_env();

View File

@ -60,6 +60,7 @@ private:
jmethodID _request_permission = nullptr; jmethodID _request_permission = nullptr;
jmethodID _request_permissions = nullptr; jmethodID _request_permissions = nullptr;
jmethodID _get_granted_permissions = nullptr; jmethodID _get_granted_permissions = nullptr;
jmethodID _get_ca_certificates = nullptr;
jmethodID _init_input_devices = nullptr; jmethodID _init_input_devices = nullptr;
jmethodID _get_surface = nullptr; jmethodID _get_surface = nullptr;
jmethodID _is_activity_resumed = nullptr; jmethodID _is_activity_resumed = nullptr;
@ -98,6 +99,7 @@ public:
bool request_permission(const String &p_name); bool request_permission(const String &p_name);
bool request_permissions(); bool request_permissions();
Vector<String> get_granted_permissions() const; Vector<String> get_granted_permissions() const;
String get_ca_certificates() const;
void init_input_devices(); void init_input_devices();
jobject get_surface(); jobject get_surface();
bool is_activity_resumed(); bool is_activity_resumed();

View File

@ -757,6 +757,10 @@ Error OS_Android::kill(const ProcessID &p_pid) {
return OS_Unix::kill(p_pid); return OS_Unix::kill(p_pid);
} }
String OS_Android::get_system_ca_certificates() {
return godot_java->get_ca_certificates();
}
Error OS_Android::setup_remote_filesystem(const String &p_server_host, int p_port, const String &p_password, String &r_project_path) { Error OS_Android::setup_remote_filesystem(const String &p_server_host, int p_port, const String &p_password, String &r_project_path) {
r_project_path = get_user_data_dir(); r_project_path = get_user_data_dir();
Error err = OS_Unix::setup_remote_filesystem(p_server_host, p_port, p_password, r_project_path); Error err = OS_Unix::setup_remote_filesystem(p_server_host, p_port, p_password, r_project_path);

View File

@ -160,6 +160,7 @@ public:
virtual Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) override; virtual Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) override;
virtual Error create_instance(const List<String> &p_arguments, ProcessID *r_child_id = nullptr) override; virtual Error create_instance(const List<String> &p_arguments, ProcessID *r_child_id = nullptr) override;
virtual Error kill(const ProcessID &p_pid) override; virtual Error kill(const ProcessID &p_pid) override;
virtual String get_system_ca_certificates() override;
virtual Error setup_remote_filesystem(const String &p_server_host, int p_port, const String &p_password, String &r_project_path) override; virtual Error setup_remote_filesystem(const String &p_server_host, int p_port, const String &p_password, String &r_project_path) override;

View File

@ -30,6 +30,7 @@
#include "os_linuxbsd.h" #include "os_linuxbsd.h"
#include "core/io/certs_compressed.gen.h"
#include "core/io/dir_access.h" #include "core/io/dir_access.h"
#include "main/main.h" #include "main/main.h"
#include "servers/display_server.h" #include "servers/display_server.h"
@ -1080,6 +1081,40 @@ Error OS_LinuxBSD::move_to_trash(const String &p_path) {
return OK; return OK;
} }
String OS_LinuxBSD::get_system_ca_certificates() {
String certfile;
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
// Compile time preferred certificates path.
if (!String(_SYSTEM_CERTS_PATH).is_empty() && da->file_exists(_SYSTEM_CERTS_PATH)) {
certfile = _SYSTEM_CERTS_PATH;
} else if (da->file_exists("/etc/ssl/certs/ca-certificates.crt")) {
// Debian/Ubuntu
certfile = "/etc/ssl/certs/ca-certificates.crt";
} else if (da->file_exists("/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem")) {
// Fedora
certfile = "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem";
} else if (da->file_exists("/etc/ca-certificates/extracted/tls-ca-bundle.pem")) {
// Arch Linux
certfile = "/etc/ca-certificates/extracted/tls-ca-bundle.pem";
} else if (da->file_exists("/var/lib/ca-certificates/ca-bundle.pem")) {
// openSUSE
certfile = "/var/lib/ca-certificates/ca-bundle.pem";
} else if (da->file_exists("/etc/ssl/cert.pem")) {
// FreeBSD/OpenBSD
certfile = "/etc/ssl/cert.pem";
}
if (certfile.is_empty()) {
return "";
}
Ref<FileAccess> f = FileAccess::open(certfile, FileAccess::READ);
ERR_FAIL_COND_V_MSG(f.is_null(), "", vformat("Failed to open system CA certificates file: '%s'", certfile));
return f->get_as_text();
}
OS_LinuxBSD::OS_LinuxBSD() { OS_LinuxBSD::OS_LinuxBSD() {
main_loop = nullptr; main_loop = nullptr;

View File

@ -133,6 +133,8 @@ public:
virtual Error move_to_trash(const String &p_path) override; virtual Error move_to_trash(const String &p_path) override;
virtual String get_system_ca_certificates() override;
OS_LinuxBSD(); OS_LinuxBSD();
~OS_LinuxBSD(); ~OS_LinuxBSD();
}; };

View File

@ -235,6 +235,8 @@ def configure(env: "Environment"):
"CoreMedia", "CoreMedia",
"-framework", "-framework",
"QuartzCore", "QuartzCore",
"-framework",
"Security",
] ]
) )
env.Append(LIBS=["pthread", "z"]) env.Append(LIBS=["pthread", "z"])

View File

@ -119,6 +119,8 @@ public:
virtual Error move_to_trash(const String &p_path) override; virtual Error move_to_trash(const String &p_path) override;
virtual String get_system_ca_certificates() override;
void run(); void run();
OS_MacOS(); OS_MacOS();

View File

@ -30,6 +30,7 @@
#include "os_macos.h" #include "os_macos.h"
#include "core/crypto/crypto_core.h"
#include "core/version_generated.gen.h" #include "core/version_generated.gen.h"
#include "main/main.h" #include "main/main.h"
@ -671,6 +672,34 @@ Error OS_MacOS::move_to_trash(const String &p_path) {
return OK; return OK;
} }
String OS_MacOS::get_system_ca_certificates() {
CFArrayRef result;
SecCertificateRef item;
CFDataRef der;
OSStatus ret = SecTrustCopyAnchorCertificates(&result);
ERR_FAIL_COND_V(ret != noErr, "");
CFIndex l = CFArrayGetCount(result);
String certs;
PackedByteArray pba;
for (CFIndex i = 0; i < l; i++) {
item = (SecCertificateRef)CFArrayGetValueAtIndex(result, i);
der = SecCertificateCopyData(item);
int derlen = CFDataGetLength(der);
if (pba.size() < derlen * 3) {
pba.resize(derlen * 3);
}
size_t b64len = 0;
Error err = CryptoCore::b64_encode(pba.ptrw(), pba.size(), &b64len, (unsigned char *)CFDataGetBytePtr(der), derlen);
CFRelease(der);
ERR_CONTINUE(err != OK);
certs += "-----BEGIN CERTIFICATE-----\n" + String((char *)pba.ptr(), b64len) + "\n-----END CERTIFICATE-----\n";
}
CFRelease(result);
return certs;
}
void OS_MacOS::run() { void OS_MacOS::run() {
if (!main_loop) { if (!main_loop) {
return; return;

View File

@ -413,6 +413,7 @@ def configure_msvc(env, vcvars_msvc_config):
"dxguid", "dxguid",
"imm32", "imm32",
"bcrypt", "bcrypt",
"Crypt32",
"Avrt", "Avrt",
"dwmapi", "dwmapi",
"dwrite", "dwrite",
@ -592,6 +593,7 @@ def configure_mingw(env):
"ksuser", "ksuser",
"imm32", "imm32",
"bcrypt", "bcrypt",
"crypt32",
"avrt", "avrt",
"uuid", "uuid",
"dwmapi", "dwmapi",

View File

@ -55,6 +55,7 @@
#include <regstr.h> #include <regstr.h>
#include <shlobj.h> #include <shlobj.h>
#include <wbemcli.h> #include <wbemcli.h>
#include <wincrypt.h>
#ifdef DEBUG_ENABLED #ifdef DEBUG_ENABLED
#pragma pack(push, before_imagehlp, 8) #pragma pack(push, before_imagehlp, 8)
@ -1677,6 +1678,26 @@ Error OS_Windows::move_to_trash(const String &p_path) {
return OK; return OK;
} }
String OS_Windows::get_system_ca_certificates() {
HCERTSTORE cert_store = CertOpenSystemStoreA(0, "ROOT");
ERR_FAIL_COND_V_MSG(!cert_store, "", "Failed to read the root certificate store.");
String certs;
PCCERT_CONTEXT curr = CertEnumCertificatesInStore(cert_store, nullptr);
while (curr) {
DWORD size = 0;
bool success = CryptBinaryToStringA(curr->pbCertEncoded, curr->cbCertEncoded, CRYPT_STRING_BASE64HEADER | CRYPT_STRING_NOCR, nullptr, &size);
ERR_CONTINUE(!success);
PackedByteArray pba;
pba.resize(size);
CryptBinaryToStringA(curr->pbCertEncoded, curr->cbCertEncoded, CRYPT_STRING_BASE64HEADER | CRYPT_STRING_NOCR, (char *)pba.ptrw(), &size);
certs += String((char *)pba.ptr(), size);
curr = CertEnumCertificatesInStore(cert_store, curr);
}
CertCloseStore(cert_store, 0);
return certs;
}
OS_Windows::OS_Windows(HINSTANCE _hInstance) { OS_Windows::OS_Windows(HINSTANCE _hInstance) {
hInstance = _hInstance; hInstance = _hInstance;

View File

@ -226,6 +226,8 @@ public:
virtual Error move_to_trash(const String &p_path) override; virtual Error move_to_trash(const String &p_path) override;
virtual String get_system_ca_certificates() override;
void set_main_window(HWND p_main_window) { main_window = p_main_window; } void set_main_window(HWND p_main_window) { main_window = p_main_window; }
HINSTANCE get_hinstance() { return hInstance; } HINSTANCE get_hinstance() { return hInstance; }