diff --git a/direct/src/p3d/AppRunner.py b/direct/src/p3d/AppRunner.py index ee53960139..e0806371c0 100644 --- a/direct/src/p3d/AppRunner.py +++ b/direct/src/p3d/AppRunner.py @@ -474,9 +474,16 @@ class AppRunner(DirectObject): self.instanceId = instanceId self.tokens = tokens - self.tokenDict = dict(tokens) self.argv = argv + # We build up a token dictionary with care, so that if a given + # token appears twice in the token list, we record only the + # first value, not the second or later. This is consistent + # with the internal behavior of the core API. + self.tokenDict = {} + for token, keyword in tokens: + self.tokenDict.setdefault(token, keyword) + # Also store the arguments on sys, for applications that # aren't instance-ready. sys.argv = argv diff --git a/direct/src/p3d/panda3d.pdef b/direct/src/p3d/panda3d.pdef index 3d94bd725e..fa72c90029 100755 --- a/direct/src/p3d/panda3d.pdef +++ b/direct/src/p3d/panda3d.pdef @@ -46,10 +46,11 @@ class images(package): print "Could not locate %s" % (filename) # Also make a few special cases. We use the same default image - # for download, ready, and launch. + # for download, ready, unauth, and launch. download = configDict.get('download_img', None) if download: configDict['ready_img'] = download + configDict['unauth_img'] = download configDict['launch_img'] = download config(**configDict) diff --git a/direct/src/plugin/load_plugin.cxx b/direct/src/plugin/load_plugin.cxx index efeb683458..76e6ea52ac 100755 --- a/direct/src/plugin/load_plugin.cxx +++ b/direct/src/plugin/load_plugin.cxx @@ -127,7 +127,7 @@ load_plugin(const string &p3d_plugin_filename, const string &contents_filename, const string &download_url, bool verify_contents, const string &platform, const string &log_directory, const string &log_basename, - bool keep_cwd) { + bool trusted_environment) { string filename = p3d_plugin_filename; if (filename.empty()) { // Look for the plugin along the path. @@ -301,7 +301,7 @@ load_plugin(const string &p3d_plugin_filename, if (!P3D_initialize(P3D_API_VERSION, contents_filename.c_str(), download_url.c_str(), verify_contents, platform.c_str(), log_directory.c_str(), log_basename.c_str(), - keep_cwd)) { + trusted_environment)) { // Oops, failure to initialize. cerr << "Failed to initialize plugin (wrong API version?)\n"; unload_plugin(); diff --git a/direct/src/plugin/load_plugin.h b/direct/src/plugin/load_plugin.h index 25dbac4cb5..d7b4845668 100755 --- a/direct/src/plugin/load_plugin.h +++ b/direct/src/plugin/load_plugin.h @@ -63,7 +63,7 @@ load_plugin(const string &p3d_plugin_filename, const string &contents_filename, const string &download_url, bool verify_contents, const string &platform, const string &log_directory, const string &log_basename, - bool keep_cwd); + bool trusted_environment); void unload_plugin(); bool is_plugin_loaded(); diff --git a/direct/src/plugin/p3dHost.I b/direct/src/plugin/p3dHost.I index 10041260d1..e4d36ec8fc 100644 --- a/direct/src/plugin/p3dHost.I +++ b/direct/src/plugin/p3dHost.I @@ -71,17 +71,3 @@ inline bool P3DHost:: has_contents_file() const { return (_xcontents != NULL); } - -//////////////////////////////////////////////////////////////////// -// Function: P3DHost::encode_hexdigit -// Access: Private -// Description: Returns the hex digit corresponding to the -// indicated integer value. -//////////////////////////////////////////////////////////////////// -inline char P3DHost:: -encode_hexdigit(int c) { - if (c >= 10) { - return c - 10 + 'a'; - } - return c + '0'; -} diff --git a/direct/src/plugin/p3dHost.cxx b/direct/src/plugin/p3dHost.cxx index 65954330ef..310da6d10d 100644 --- a/direct/src/plugin/p3dHost.cxx +++ b/direct/src/plugin/p3dHost.cxx @@ -293,8 +293,8 @@ determine_host_dir() { for (size_t i = 0; i < keep_hash; ++i) { int high = (md[i] >> 4) & 0xf; int low = md[i] & 0xf; - _host_dir += encode_hexdigit(high); - _host_dir += encode_hexdigit(low); + _host_dir += P3DInstanceManager::encode_hexdigit(high); + _host_dir += P3DInstanceManager::encode_hexdigit(low); } } diff --git a/direct/src/plugin/p3dHost.h b/direct/src/plugin/p3dHost.h index d8376d57b4..bfd1dbe140 100644 --- a/direct/src/plugin/p3dHost.h +++ b/direct/src/plugin/p3dHost.h @@ -56,7 +56,6 @@ private: static string standardize_filename(const string &filename); static bool copy_file(const string &from_filename, const string &to_filename); - static inline char encode_hexdigit(int c); private: string _host_dir; diff --git a/direct/src/plugin/p3dInstance.cxx b/direct/src/plugin/p3dInstance.cxx index 8f340b62e1..678b6f2d58 100644 --- a/direct/src/plugin/p3dInstance.cxx +++ b/direct/src/plugin/p3dInstance.cxx @@ -55,6 +55,10 @@ const char *P3DInstance::_image_type_names[P3DInstance::IT_num_image_types] = { "play_ready", "play_rollover", "play_click", + "unauth", + "auth_ready", + "auth_rollover", + "auth_click", "none", // Not really used. }; @@ -80,6 +84,7 @@ P3DInstance(P3D_request_ready_func *func, _current_button_image = IT_none; _got_fparams = false; _got_wparams = false; + _p3d_trusted = false; _fparams.set_tokens(tokens, num_tokens); _fparams.set_args(argc, argv); @@ -89,6 +94,7 @@ P3DInstance(P3D_request_ready_func *func, _hidden = false; _allow_python_dev = false; _auto_start = false; + _auth_button_approved = false; _session = NULL; _panda3d = NULL; _splash_window = NULL; @@ -116,6 +122,10 @@ P3DInstance(P3D_request_ready_func *func, _panda_script_object->set_string_property("downloadPackageDisplayName", ""); _panda_script_object->set_bool_property("downloadComplete", false); _panda_script_object->set_string_property("status", "initial"); + + // We'll start off with the "download" image displayed in the splash + // window (when it opens), until we get stuff downloaded. + set_background_image(IT_download); } //////////////////////////////////////////////////////////////////// @@ -234,50 +244,24 @@ set_p3d_url(const string &p3d_url) { void P3DInstance:: set_p3d_filename(const string &p3d_filename) { _fparams.set_p3d_filename(p3d_filename); + _got_fparams = true; _panda_script_object->set_float_property("instanceDownloadProgress", 1.0); - // This also sets up some internal data based on the contents of the - // above file and the associated tokens. - - // Extract the application desc file from the p3d file. - P3DMultifileReader reader; - stringstream sstream; - if (!reader.extract_one(p3d_filename, sstream, "p3d_info.xml")) { - nout << "No p3d_info.xml file found in " << p3d_filename << "\n"; - } else { - sstream.seekg(0); - TiXmlDocument doc; - sstream >> doc; - - // This also starts required packages downloading. When all - // packages have been installed, we will start the instance. - scan_app_desc_file(&doc); - } - - // For the moment, all sessions will be unique. TODO: support - // multiple instances per session. - P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr(); - ostringstream strm; - strm << inst_mgr->get_unique_id(); - _session_key = strm.str(); - - // Until we've done all of the above processing, we haven't fully - // committed to having fparams. (Setting this flag down here - // instead of up there avoids starting the instance in - // scan_app_desc_file(), before we've had a chance to finish - // processing this method.) - _got_fparams = true; - // Generate a special notification: onpluginload, indicating the // plugin has read its parameters and is ready to be queried (even // if Python has not yet started). send_notify("onpluginload"); - // Now that we're all set up, start the instance if we're fully - // downloaded. - if (get_packages_ready() && _got_wparams) { - ready_to_start(); + if (!_mf_reader.open_read(_fparams.get_p3d_filename())) { + nout << "Couldn't read " << _fparams.get_p3d_filename() << "\n"; + return; + } + + if (check_p3d_signature()) { + mark_p3d_trusted(); + } else { + mark_p3d_untrusted(); } } @@ -356,7 +340,7 @@ set_wparams(const P3DWindowParams &wparams) { _session->send_command(doc); } - if (get_packages_ready() && _got_fparams) { + if (get_packages_ready() && _p3d_trusted) { ready_to_start(); } } @@ -904,29 +888,174 @@ make_xml() { //////////////////////////////////////////////////////////////////// void P3DInstance:: splash_button_clicked() { - if (_session == NULL) { + if (!_p3d_trusted) { + auth_button_clicked(); + } else if (_session == NULL) { play_button_clicked(); } } +//////////////////////////////////////////////////////////////////// +// Function: P3DInstance::auth_button_clicked +// Access: Public +// Description: Called to authorize the p3d file by the user clicking +// the red "auth" button. +//////////////////////////////////////////////////////////////////// +void P3DInstance:: +auth_button_clicked() { + // Here's where we need to invoke the authorization program. + cerr << "auth clicked\n"; + + // After the authorization program has returned, check the signature + // again. + if (check_p3d_signature()) { + // Set this flag to indicate that the user has clicked on the red + // "auth" button an successfully approved the application. This + // eliminates the need to click on the green "start" button. + _auth_button_approved = true; + mark_p3d_trusted(); + } else { + mark_p3d_untrusted(); + } +} + //////////////////////////////////////////////////////////////////// // Function: P3DInstance::play_button_clicked // Access: Public // Description: Called to start the game by the user clicking the -// "play" button, or by JavaScript calling start(). +// green "play" button, or by JavaScript calling +// start(). //////////////////////////////////////////////////////////////////// void P3DInstance:: play_button_clicked() { if (_session == NULL) { - if (_splash_window != NULL) { - _splash_window->set_button_active(false); - } + set_button_image(IT_none); set_background_image(IT_launch); P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr(); inst_mgr->start_instance(this); } } +//////////////////////////////////////////////////////////////////// +// Function: P3DInstance::check_p3d_signature +// Access: Private +// Description: Checks the signature(s) encoded in the p3d file, and +// looks to see if any of them are recognized. Returns +// true if the signature is recognized and the file +// should be trusted, false otherwise. +//////////////////////////////////////////////////////////////////// +bool P3DInstance:: +check_p3d_signature() { + P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr(); + if (inst_mgr->get_trusted_environment()) { + // If we're in a trusted environment (e.g. the panda3d command + // line, where we've already downloaded the p3d file separately), + // then everything is approved. + return true; + } + + // Temporary hack: disabling further security checks until this code + // is complete. + return true; + + nout << "check_p3d_signature\n"; + int num_signatures = _mf_reader.get_num_signatures(); + nout << "file has " << num_signatures << " signatures\n"; + + for (int i = 0; i < num_signatures; ++i) { + const P3DMultifileReader::CertChain &chain = _mf_reader.get_signature(i); + + // Here's a certificate that has signed this multifile. + X509 *cert = chain[0]._cert; + + // Look up the certificate to see if we've stored a copy in our + // certs dir. + if (inst_mgr->find_cert(cert)) { + return true; + } + } + + // Couldn't find any approved certificates. + return false; +} + +//////////////////////////////////////////////////////////////////// +// Function: P3DInstance::mark_p3d_untrusted +// Access: Private +// Description: This is called internally when it has been determined +// that the p3d file can't (yet) be trusted, for +// instance because it lacks a signature, or because it +// is signed by an unrecognized certificate. This puts +// up the red "auth" button and waits for the user to +// approve the app before continuing. +//////////////////////////////////////////////////////////////////// +void P3DInstance:: +mark_p3d_untrusted() { + // Failed test. + nout << "p3d untrusted\n"; + set_background_image(IT_unauth); + set_button_image(IT_auth_ready); + make_splash_window(); +} + +//////////////////////////////////////////////////////////////////// +// Function: P3DInstance::mark_p3d_trusted +// Access: Private +// Description: This is called internally when it has been determined +// that the p3d file can be trusted and started. When +// this is called, the p3d file will be examined and +// made ready to start; it will not be started until +// this is called. +//////////////////////////////////////////////////////////////////// +void P3DInstance:: +mark_p3d_trusted() { + nout << "p3d trusted\n"; + // Only call this once. + assert(!_p3d_trusted); + + // Extract the application desc file from the p3d file. + stringstream sstream; + if (!_mf_reader.extract_one(sstream, "p3d_info.xml")) { + nout << "No p3d_info.xml file found in " << _fparams.get_p3d_filename() << "\n"; + // TODO: fail. + + } else { + sstream.seekg(0); + TiXmlDocument doc; + sstream >> doc; + + // This also starts required packages downloading. When all + // packages have been installed, we will start the instance. + scan_app_desc_file(&doc); + } + + // Now we've got no further need to keep the _mf_reader open. + _mf_reader.close(); + + // For the moment, all sessions will be unique. TODO: support + // multiple instances per session. + P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr(); + ostringstream strm; + strm << inst_mgr->get_unique_id(); + _session_key = strm.str(); + + // Until we've done all of the above processing, we haven't fully + // committed to setting the trusted flag. (Setting this flag down + // here instead of up there avoids starting the instance in + // scan_app_desc_file(), before we've had a chance to finish + // processing this method.) + _p3d_trusted = true; + + // Notify JS that we've accepted the trust of the p3d file. + send_notify("ontrust"); + + // Now that we're all set up, start the instance if we're fully + // downloaded. + if (get_packages_ready() && _got_wparams) { + ready_to_start(); + } +} + //////////////////////////////////////////////////////////////////// // Function: P3DInstance::scan_app_desc_file // Access: Private @@ -970,10 +1099,15 @@ scan_app_desc_file(TiXmlDocument *doc) { if (_fparams.lookup_token_int("auto_start") != 0) { _auto_start = true; } + if (_auth_button_approved) { + // But finally, if the user has already clicked through the red + // "auth" button, no need to present him/her with another green + // "play" button as well. + _auto_start = true; + } + nout << "_auto_start = " << _auto_start << "\n"; - // But auto_start will be set false if the p3d file has not been - // signed by an approved signature. TODO. if (_hidden && _got_wparams) { _wparams.set_window_type(P3D_WT_hidden); @@ -1284,9 +1418,10 @@ make_splash_window() { return; } if (_wparams.get_window_type() != P3D_WT_embedded && - !_stuff_to_download && _auto_start) { + !_stuff_to_download && _auto_start && _p3d_trusted) { // If it's a toplevel or fullscreen window, then we don't want a - // splash window until we have stuff to download. + // splash window unless we have stuff to download, or a button to + // display. return; } @@ -1296,10 +1431,6 @@ make_splash_window() { P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr(); - // Direct the "download" image to the background slot on the splash - // window for now, while we perform the download. - set_background_image(IT_download); - // Go get the required images. bool any_standard_images = false; for (int i = 0; i < (int)IT_none; ++i) { @@ -1344,6 +1475,10 @@ make_splash_window() { _image_package->add_instance(this); } } + + if (_current_button_image != IT_none) { + _splash_window->set_button_active(true); + } } //////////////////////////////////////////////////////////////////// @@ -1357,6 +1492,7 @@ make_splash_window() { void P3DInstance:: set_background_image(ImageType image_type) { if (image_type != _current_background_image) { + nout << "setting background to " << _image_type_names[image_type] << "\n"; // Remove the previous image. _image_files[_current_background_image]._image_placement = P3DSplashWindow::IP_none; @@ -1387,6 +1523,7 @@ set_background_image(ImageType image_type) { void P3DInstance:: set_button_image(ImageType image_type) { if (image_type != _current_button_image) { + nout << "setting button to " << _image_type_names[image_type] << "\n"; // Remove the previous image. _image_files[_current_button_image]._image_placement = P3DSplashWindow::IP_none; if (_current_button_image != IT_none) { @@ -1408,10 +1545,20 @@ set_button_image(ImageType image_type) { _splash_window->set_image_filename(_image_files[_current_button_image]._filename, P3DSplashWindow::IP_button_ready); _splash_window->set_image_filename(_image_files[_current_button_image + 1]._filename, P3DSplashWindow::IP_button_rollover); _splash_window->set_image_filename(_image_files[_current_button_image + 2]._filename, P3DSplashWindow::IP_button_click); + _splash_window->set_button_active(true); } else { - _splash_window->set_image_filename(string(), P3DSplashWindow::IP_button_ready); - _splash_window->set_image_filename(string(), P3DSplashWindow::IP_button_rollover); - _splash_window->set_image_filename(string(), P3DSplashWindow::IP_button_click); + _splash_window->set_button_active(false); + } + } + + } else { + // We're not changing the button graphic, but we might be + // re-activating it. + if (_splash_window != NULL) { + if (_current_button_image != IT_none) { + _splash_window->set_button_active(true); + } else { + _splash_window->set_button_active(false); } } } @@ -1532,7 +1679,7 @@ start_next_download() { _splash_window->set_install_label(""); } - if (_got_wparams && _got_fparams) { + if (_got_wparams && _p3d_trusted) { ready_to_start(); } } @@ -1556,7 +1703,6 @@ ready_to_start() { // We're fully downloaded, and waiting for the user to click play. set_background_image(IT_ready); set_button_image(IT_play_ready); - _splash_window->set_button_active(true); } } diff --git a/direct/src/plugin/p3dInstance.h b/direct/src/plugin/p3dInstance.h index 1b530368fe..3219c6aac5 100644 --- a/direct/src/plugin/p3dInstance.h +++ b/direct/src/plugin/p3dInstance.h @@ -22,6 +22,7 @@ #include "p3dReferenceCount.h" #include "p3dSplashWindow.h" #include "p3dTemporaryFile.h" +#include "p3dMultifileReader.h" #include "get_tinyxml.h" #ifdef __APPLE__ @@ -101,6 +102,7 @@ public: TiXmlElement *make_xml(); void splash_button_clicked(); + void auth_button_clicked(); void play_button_clicked(); private: @@ -126,6 +128,7 @@ private: // The different kinds of image files we download for the splash // window. enum ImageType { + // Also update _image_type_names when you update this list. IT_download, IT_ready, IT_failed, @@ -133,10 +136,17 @@ private: IT_play_ready, IT_play_rollover, IT_play_click, + IT_unauth, + IT_auth_ready, + IT_auth_rollover, + IT_auth_click, IT_none, // Must be the last value IT_num_image_types, // Not a real value }; + bool check_p3d_signature(); + void mark_p3d_untrusted(); + void mark_p3d_trusted(); void scan_app_desc_file(TiXmlDocument *doc); void send_browser_script_object(); @@ -192,16 +202,20 @@ private: bool _got_fparams; P3DFileParams _fparams; + P3DMultifileReader _mf_reader; bool _got_wparams; P3DWindowParams _wparams; + bool _p3d_trusted; + int _instance_id; string _session_key; string _log_basename; bool _hidden; bool _allow_python_dev; bool _auto_start; + bool _auth_button_approved; P3DSession *_session; diff --git a/direct/src/plugin/p3dInstanceManager.I b/direct/src/plugin/p3dInstanceManager.I index b6571a9e4e..bc511e33f2 100644 --- a/direct/src/plugin/p3dInstanceManager.I +++ b/direct/src/plugin/p3dInstanceManager.I @@ -105,17 +105,21 @@ get_log_pathname() const { } //////////////////////////////////////////////////////////////////// -// Function: P3DInstanceManager::get_keep_cwd +// Function: P3DInstanceManager::get_trusted_environment // Access: Public -// Description: Returns the value of the keep_cwd flag passed to the -// constructor. This is true if the original working -// directory was valuable and meaningful, and should be -// preserved; or false if it is meaningless and should -// be changed. +// Description: Returns the value of the trusted_environment flag +// passed to the constructor. If this is true, it means +// the environment we are running in is trusted and the +// p3d file is already vetted. This means the current +// working directory will remain unchanged, and the p3d +// file will be run without checking its signature. +// +// This should generally be true only when run by +// panda3d.exe, and not when run by the web plugin. //////////////////////////////////////////////////////////////////// inline bool P3DInstanceManager:: -get_keep_cwd() const { - return _keep_cwd; +get_trusted_environment() const { + return _trusted_environment; } //////////////////////////////////////////////////////////////////// @@ -165,3 +169,17 @@ new_bool_object(bool value) { P3D_OBJECT_INCREF(obj); return obj; } + +//////////////////////////////////////////////////////////////////// +// Function: P3DInstanceManager::encode_hexdigit +// Access: Public +// Description: Returns the hex digit corresponding to the +// indicated integer value. +//////////////////////////////////////////////////////////////////// +inline char P3DInstanceManager:: +encode_hexdigit(int c) { + if (c >= 10) { + return c - 10 + 'a'; + } + return c + '0'; +} diff --git a/direct/src/plugin/p3dInstanceManager.cxx b/direct/src/plugin/p3dInstanceManager.cxx index 367c1376b0..c4296a1b75 100644 --- a/direct/src/plugin/p3dInstanceManager.cxx +++ b/direct/src/plugin/p3dInstanceManager.cxx @@ -35,8 +35,11 @@ #else #include #include +#include #endif +#include + static ofstream logfile; ostream *nout_stream = &logfile; @@ -52,7 +55,7 @@ P3DInstanceManager() { _is_initialized = false; _next_temp_filename_counter = 1; _unique_id = 0; - _keep_cwd = false; + _trusted_environment = false; _notify_thread_continue = false; _started_notify_thread = false; @@ -157,8 +160,8 @@ bool P3DInstanceManager:: initialize(const string &contents_filename, const string &download_url, bool verify_contents, const string &platform, const string &log_directory, - const string &log_basename, bool keep_cwd) { - _keep_cwd = keep_cwd; + const string &log_basename, bool trusted_environment) { + _trusted_environment = trusted_environment; _root_dir = find_root_dir(); _verify_contents = verify_contents; _platform = platform; @@ -256,6 +259,12 @@ initialize(const string &contents_filename, const string &download_url, return false; } + // Make the certificate directory. + _certs_dir = _root_dir + "/certs"; + if (!mkdir_complete(_certs_dir, nout)) { + nout << "Couldn't mkdir " << _certs_dir << "\n"; + } + _is_initialized = true; if (!_verify_contents && @@ -645,6 +654,176 @@ release_temp_filename(const string &filename) { unlink(filename.c_str()); } +//////////////////////////////////////////////////////////////////// +// Function: P3DInstanceManager::find_cert +// Access: Public +// Description: Looks for the particular certificate in the cache of +// recognized certificates. Returns true if it is +// found, false if not. +//////////////////////////////////////////////////////////////////// +bool P3DInstanceManager:: +find_cert(X509 *cert) { + // First, we need the DER representation. + string der = cert_to_der(cert); + + // If we've previously found this certificate, we don't have to hit + // disk again. + ApprovedCerts::iterator ci = _approved_certs.find(der); + if (ci != _approved_certs.end()) { + return true; + } + + // Well, we haven't found it already. Look for it on disk. For + // this, we hash the cert into a hex string. This is similar to + // OpenSSL's get_by_subject() approach, except we hash the whole + // cert, not just the subject. (Since we also store self-signed + // certs in this list, we can't trust the subject name alone.) + + static const size_t hash_size = 16; + unsigned char md[hash_size]; + + MD5_CTX ctx; + MD5_Init(&ctx); + MD5_Update(&ctx, der.data(), der.size()); + MD5_Final(md, &ctx); + + string basename; + static const size_t keep_hash = 6; + for (size_t i = 0; i < keep_hash; ++i) { + int high = (md[i] >> 4) & 0xf; + int low = md[i] & 0xf; + basename += P3DInstanceManager::encode_hexdigit(high); + basename += P3DInstanceManager::encode_hexdigit(low); + } + + string this_cert_dir = _certs_dir + "/" + basename; + nout << "looking in " << this_cert_dir << "\n"; + + vector contents; + scan_directory(this_cert_dir, contents); + + // Now look at each of the files in this directory and see if any of + // them matches the certificate. + vector::iterator si; + for (si = contents.begin(); si != contents.end(); ++si) { + string filename = this_cert_dir + "/" + (*si); + X509 *x509 = NULL; + FILE *fp = fopen(filename.c_str(), "r"); + if (fp != NULL) { + x509 = PEM_read_X509(fp, NULL, NULL, (void *)""); + fclose(fp); + } + + if (x509 != NULL) { + string der2 = cert_to_der(x509); + // We might as well save this cert in the table for next time, + // even if it's not the one we're looking for right now. + _approved_certs.insert(der2); + + if (der == der2) { + return true; + } + } + } + + // Nothing matched. + return false; +} + +//////////////////////////////////////////////////////////////////// +// Function: P3DInstanceManager::cert_to_der +// Access: Public, Static +// Description: Converts the indicated certificate to its binary DER +// representation. +//////////////////////////////////////////////////////////////////// +string P3DInstanceManager:: +cert_to_der(X509 *cert) { + int buffer_size = i2d_X509(cert, NULL); + unsigned char *buffer = new unsigned char[buffer_size]; + unsigned char *p = buffer; + i2d_X509(cert, &p); + + string result((char *)buffer, buffer_size); + delete[] buffer; + + return result; +} + +//////////////////////////////////////////////////////////////////// +// Function: P3DInstanceManager::scan_directory +// Access: Public, Static +// Description: Attempts to open the named filename as if it were a +// directory and looks for the non-hidden files within +// the directory. Fills the given vector up with the +// sorted list of filenames that are local to this +// directory. +// +// It is the user's responsibility to ensure that the +// contents vector is empty before making this call; +// otherwise, the new files will be appended to it. +// +// Returns true on success, false if the directory could +// not be read for some reason. +//////////////////////////////////////////////////////////////////// +bool P3DInstanceManager:: +scan_directory(const string &dirname, vector &contents) { +#ifdef _WIN32 + // Use Windows' FindFirstFile() / FindNextFile() to walk through the + // list of files in a directory. + size_t orig_size = contents.size(); + + string match = dirname + "\\*.*"; + WIN32_FIND_DATA find_data; + + HANDLE handle = FindFirstFile(match.c_str(), &find_data); + if (handle == INVALID_HANDLE_VALUE) { + if (GetLastError() == ERROR_NO_MORE_FILES) { + // No matching files is not an error. + return true; + } + return false; + } + + do { + string filename = find_data.cFileName; + if (filename != "." && filename != "..") { + contents.push_back(filename); + } + } while (FindNextFile(handle, &find_data)); + + bool scan_ok = (GetLastError() == ERROR_NO_MORE_FILES); + FindClose(handle); + + sort(contents.begin() + orig_size, contents.end()); + return scan_ok; + +#else // _WIN32 + // Use Posix's opendir() / readdir() to walk through the list of + // files in a directory. + size_t orig_size = contents.size(); + + DIR *root = opendir(dirname.c_str()); + if (root == (DIR *)NULL) { + return false; + } + + struct dirent *d; + d = readdir(root); + while (d != (struct dirent *)NULL) { + if (d->d_name[0] != '.') { + contents.push_back(d->d_name); + } + d = readdir(root); + } + + closedir(root); + + sort(contents.begin() + orig_size, contents.end()); + return true; + +#endif // _WIN32 +} + //////////////////////////////////////////////////////////////////// // Function: P3DInstanceManager::get_global_ptr // Access: Public, Static diff --git a/direct/src/plugin/p3dInstanceManager.h b/direct/src/plugin/p3dInstanceManager.h index a6da79cbf4..e8c08b4567 100644 --- a/direct/src/plugin/p3dInstanceManager.h +++ b/direct/src/plugin/p3dInstanceManager.h @@ -26,6 +26,11 @@ #include #endif +#define OPENSSL_NO_KRB5 +#include "openssl/x509.h" +#include "openssl/pem.h" +#include "openssl/md5.h" + class P3DInstance; class P3DSession; class P3DHost; @@ -49,7 +54,7 @@ public: const string &platform, const string &log_directory, const string &log_basename, - bool keep_cwd); + bool trusted_environment); inline bool is_initialized() const; inline bool get_verify_contents() const; @@ -59,7 +64,7 @@ public: inline const string &get_platform() const; inline const string &get_log_directory() const; inline const string &get_log_pathname() const; - inline bool get_keep_cwd() const; + inline bool get_trusted_environment() const; P3DInstance * create_instance(P3D_request_ready_func *func, @@ -92,9 +97,14 @@ public: string make_temp_filename(const string &extension); void release_temp_filename(const string &filename); + bool find_cert(X509 *cert); + static string cert_to_der(X509 *cert); + static P3DInstanceManager *get_global_ptr(); static void delete_global_ptr(); + static inline char encode_hexdigit(int c); + static bool scan_directory(const string &dirname, vector &contents); private: // The notify thread. This thread runs only for the purpose of // generating asynchronous notifications of requests, to callers who @@ -105,19 +115,23 @@ private: private: bool _is_initialized; string _root_dir; + string _certs_dir; bool _verify_contents; string _platform; string _log_directory; string _log_basename; string _log_pathname; string _temp_directory; - bool _keep_cwd; + bool _trusted_environment; P3D_object *_undefined_object; P3D_object *_none_object; P3D_object *_true_object; P3D_object *_false_object; + typedef set ApprovedCerts; + ApprovedCerts _approved_certs; + typedef set Instances; Instances _instances; diff --git a/direct/src/plugin/p3dMultifileReader.I b/direct/src/plugin/p3dMultifileReader.I index 9552da3063..abcb6a0983 100644 --- a/direct/src/plugin/p3dMultifileReader.I +++ b/direct/src/plugin/p3dMultifileReader.I @@ -13,6 +13,16 @@ //////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////// +// Function: P3DMultifileReader::is_open +// Access: Public +// Description: Returns true if the reader is open, false otherwise. +//////////////////////////////////////////////////////////////////// +inline bool P3DMultifileReader:: +is_open() const { + return _is_open; +} + //////////////////////////////////////////////////////////////////// // Function: P3DMultifileReader::read_uint16 // Access: Private @@ -38,3 +48,49 @@ read_uint32() { unsigned int d = _in.get(); return (d << 24) | (c << 16) | (b << 8) | a; } + +//////////////////////////////////////////////////////////////////// +// Function: P3DMultifileReader::get_last_byte_pos +// Access: Public +// Description: Returns the byte position within the Multifile of the +// last byte that contributes to this Subfile, either in +// the index record or in the subfile data. +//////////////////////////////////////////////////////////////////// +inline size_t P3DMultifileReader::Subfile:: +get_last_byte_pos() const { + return max(_index_start + _index_length, _data_start + _data_length) - 1; +} + +//////////////////////////////////////////////////////////////////// +// Function: P3DMultifileReader::CertRecord::Constructor +// Access: Public +// Description: Ownership of the X509 object is passed into the +// CertRecord; it will be freed when the CertRecord +// destructs. +//////////////////////////////////////////////////////////////////// +inline P3DMultifileReader::CertRecord:: +CertRecord(X509 *cert) : + _cert(cert) +{ +} + +//////////////////////////////////////////////////////////////////// +// Function: P3DMultifileReader::CertRecord::Copy Constructor +// Access: Public +// Description: +//////////////////////////////////////////////////////////////////// +inline P3DMultifileReader::CertRecord:: +CertRecord(const P3DMultifileReader::CertRecord ©) : + _cert(X509_dup(copy._cert)) +{ +} + +//////////////////////////////////////////////////////////////////// +// Function: P3DMultifileReader::CertRecord::Destructor +// Access: Public +// Description: +//////////////////////////////////////////////////////////////////// +inline P3DMultifileReader::CertRecord:: +~CertRecord() { + X509_free(_cert); +} diff --git a/direct/src/plugin/p3dMultifileReader.cxx b/direct/src/plugin/p3dMultifileReader.cxx index cf8d7e1e5a..b0bc1500a8 100644 --- a/direct/src/plugin/p3dMultifileReader.cxx +++ b/direct/src/plugin/p3dMultifileReader.cxx @@ -45,15 +45,46 @@ const int P3DMultifileReader::_current_minor_ver = 1; //////////////////////////////////////////////////////////////////// P3DMultifileReader:: P3DMultifileReader() { + _is_open = false; +} + +//////////////////////////////////////////////////////////////////// +// Function: P3DMultifileReader::open_read +// Access: Public +// Description: Opens the indicated file for reading. Returns true +// on success, false on failure. +//////////////////////////////////////////////////////////////////// +bool P3DMultifileReader:: +open_read(const string &pathname) { + if (_is_open) { + close(); + } + if (!read_header(pathname)) { + return false; + } + + _is_open = true; + return true; +} + +//////////////////////////////////////////////////////////////////// +// Function: P3DMultifileReader::close +// Access: Public +// Description: Closes the previously-opened file. +//////////////////////////////////////////////////////////////////// +void P3DMultifileReader:: +close() { + _in.close(); + _is_open = false; } //////////////////////////////////////////////////////////////////// // Function: P3DMultifileReader::extract_all // Access: Public -// Description: Reads the named multifile, and extracts all the -// expected extractable components within it to the -// indicated directory. Returns true on success, false -// on failure. +// Description: Reads the multifile, and extracts all the expected +// extractable components within it to the indicated +// directory. Returns true on success, false on +// failure. // // The parameters package, start_progress, and // progress_size are provided to make the appropriate @@ -61,9 +92,10 @@ P3DMultifileReader() { // during this operation. //////////////////////////////////////////////////////////////////// bool P3DMultifileReader:: -extract_all(const string &pathname, const string &to_dir, +extract_all(const string &to_dir, P3DPackage *package, double start_progress, double progress_size) { - if (!read_header(pathname)) { + assert(_is_open); + if (_in.fail()) { return false; } @@ -118,13 +150,14 @@ extract_all(const string &pathname, const string &to_dir, //////////////////////////////////////////////////////////////////// // Function: P3DMultifileReader::extract_one // Access: Public -// Description: Reads the named multifile, and extracts only the -// named component to the indicated stream. Returns -// true on success, false on failure. +// Description: Reads the multifile, and extracts only the named +// component to the indicated stream. Returns true on +// success, false on failure. //////////////////////////////////////////////////////////////////// bool P3DMultifileReader:: -extract_one(const string &pathname, ostream &out, const string &filename) { - if (!read_header(pathname)) { +extract_one(ostream &out, const string &filename) { + assert(_is_open); + if (_in.fail()) { return false; } @@ -141,6 +174,42 @@ extract_one(const string &pathname, ostream &out, const string &filename) { return false; } +//////////////////////////////////////////////////////////////////// +// Function: P3DMultifileReader::get_num_signatures +// Access: Published +// Description: Returns the number of matching signatures found on +// the Multifile. These signatures may be iterated via +// get_signature() and related methods. +// +// A signature on this list is guaranteed to match the +// Multifile contents, proving that the Multifile has +// been unmodified since the signature was applied. +// However, this does not guarantee that the certificate +// itself is actually from who it says it is from; only +// that it matches the Multifile contents. See +// validate_signature_certificate() to authenticate a +// particular certificate. +//////////////////////////////////////////////////////////////////// +int P3DMultifileReader:: +get_num_signatures() const { + assert(_is_open); + ((P3DMultifileReader *)this)->check_signatures(); + return _signatures.size(); +} + +//////////////////////////////////////////////////////////////////// +// Function: P3DMultifileReader::get_signature +// Access: Published +// Description: Returns the nth signature found on the Multifile. +// See the comments in get_num_signatures(). +//////////////////////////////////////////////////////////////////// +const P3DMultifileReader::CertChain &P3DMultifileReader:: +get_signature(int n) const { + static CertChain error_chain; + assert(n >= 0 && n < (int)_signatures.size()); + return _signatures[n]; +} + //////////////////////////////////////////////////////////////////// // Function: P3DMultifileReader::read_header // Access: Private @@ -150,7 +219,10 @@ extract_one(const string &pathname, ostream &out, const string &filename) { //////////////////////////////////////////////////////////////////// bool P3DMultifileReader:: read_header(const string &pathname) { + assert(!_is_open); _subfiles.clear(); + _cert_special.clear(); + _signatures.clear(); _in.open(pathname.c_str(), ios::in | ios::binary); if (!_in) { @@ -228,16 +300,19 @@ read_header(const string &pathname) { //////////////////////////////////////////////////////////////////// bool P3DMultifileReader:: read_index() { + _last_data_byte = 0; unsigned int next_entry = read_uint32(); if (!_in) { return false; } while (next_entry != 0) { Subfile s; - s._start = read_uint32(); - s._length = read_uint32(); + s._index_start = (size_t)_in.tellg(); + s._index_length = 0; + s._data_start = read_uint32(); + s._data_length = read_uint32(); unsigned int flags = read_uint16(); - if ((flags & 0x18) != 0) { + if ((flags & (SF_compressed | SF_encrypted)) != 0) { // Skip over the uncompressed length. read_uint32(); } @@ -255,9 +330,19 @@ read_index() { s._filename = string(buffer, name_length); delete[] buffer; - if (flags == 0) { - // We can only support subfiles with no particular flags set. - _subfiles.push_back(s); + s._index_length = (size_t)_in.tellg() - s._index_start; + + if (flags & SF_signature) { + // A subfile with this bit set is a signature. + _cert_special.push_back(s); + } else { + // Otherwise, it's a regular file. + _last_data_byte = max(_last_data_byte, s.get_last_byte_pos()); + + if (flags == 0) { + // We can only support subfiles with no particular flags set. + _subfiles.push_back(s); + } } _in.seekg(next_entry); @@ -279,12 +364,12 @@ read_index() { //////////////////////////////////////////////////////////////////// bool P3DMultifileReader:: extract_subfile(ostream &out, const Subfile &s) { - _in.seekg(s._start); + _in.seekg(s._data_start); - static const size_t buffer_size = 1024; + static const size_t buffer_size = 4096; char buffer[buffer_size]; - size_t remaining_data = s._length; + size_t remaining_data = s._data_length; _in.read(buffer, min(buffer_size, remaining_data)); size_t count = _in.gcount(); while (count != 0) { @@ -302,3 +387,120 @@ extract_subfile(ostream &out, const Subfile &s) { return true; } + +//////////////////////////////////////////////////////////////////// +// Function: P3DMultifileReader::check_signatures +// Access: Private +// Description: Walks through the list of _cert_special entries in +// the Multifile, moving any valid signatures found to +// _signatures. After this call, _cert_special will be +// empty. +// +// This does not check the validity of the certificates +// themselves. It only checks that they correctly sign +// the Multifile contents. +//////////////////////////////////////////////////////////////////// +void P3DMultifileReader:: +check_signatures() { + Subfiles::iterator pi; + + for (pi = _cert_special.begin(); pi != _cert_special.end(); ++pi) { + Subfile *subfile = &(*pi); + + // Extract the signature data and certificate separately. + _in.seekg(subfile->_data_start); + size_t sig_size = read_uint32(); + char *sig = new char[sig_size]; + _in.read(sig, sig_size); + if (_in.gcount() != sig_size) { + nout << "read failure\n"; + delete[] sig; + return; + } + + size_t num_certs = read_uint32(); + + // Read the remaining buffer of certificate data. + size_t bytes_read = (size_t)_in.tellg() - subfile->_data_start; + size_t buffer_size = subfile->_data_length - bytes_read; + char *buffer = new char[buffer_size]; + _in.read(buffer, buffer_size); + if (_in.gcount() != buffer_size) { + nout << "read failure\n"; + delete[] sig; + delete[] buffer; + return; + } + + // Now convert each of the certificates to an X509 object, and + // store it in our CertChain. + CertChain chain; + EVP_PKEY *pkey = NULL; + if (buffer_size > 0) { +#if OPENSSL_VERSION_NUMBER >= 0x00908000L + // Beginning in 0.9.8, d2i_X509() accepted a const unsigned char **. + const unsigned char *bp, *bp_end; +#else + // Prior to 0.9.8, d2i_X509() accepted an unsigned char **. + unsigned char *bp, *bp_end; +#endif + bp = (unsigned char *)&buffer[0]; + bp_end = bp + buffer_size; + X509 *x509 = d2i_X509(NULL, &bp, bp_end - bp); + while (num_certs > 0 && x509 != NULL) { + chain.push_back(CertRecord(x509)); + --num_certs; + x509 = d2i_X509(NULL, &bp, bp_end - bp); + } + if (num_certs != 0 || x509 != NULL) { + nout << "Extra data in signature record.\n"; + } + } + + delete[] buffer; + + if (!chain.empty()) { + pkey = X509_get_pubkey(chain[0]._cert); + } + + if (pkey != NULL) { + EVP_MD_CTX *md_ctx; +#if OPENSSL_VERSION_NUMBER >= 0x00907000L + md_ctx = EVP_MD_CTX_create(); +#else + md_ctx = new EVP_MD_CTX; +#endif + EVP_VerifyInit(md_ctx, EVP_sha1()); + + // Read and hash the multifile contents, but only up till + // _last_data_byte. + _in.seekg(0); + streampos bytes_remaining = (streampos)_last_data_byte; + static const size_t buffer_size = 4096; + char buffer[buffer_size]; + _in.read(buffer, min((streampos)buffer_size, bytes_remaining)); + size_t count = _in.gcount(); + while (count != 0) { + assert(count <= buffer_size); + EVP_VerifyUpdate(md_ctx, buffer, count); + bytes_remaining -= count; + _in.read(buffer, min((streampos)buffer_size, bytes_remaining)); + count = _in.gcount(); + } + assert(bytes_remaining == (streampos)0); + + // Now check that the signature matches the hash. + int verify_result = + EVP_VerifyFinal(md_ctx, (unsigned char *)sig, + sig_size, pkey); + if (verify_result == 1) { + // The signature matches; save the certificate and its chain. + _signatures.push_back(chain); + } + } + + delete[] sig; + } + + _cert_special.clear(); +} diff --git a/direct/src/plugin/p3dMultifileReader.h b/direct/src/plugin/p3dMultifileReader.h index bb7dd13111..c90a2ca099 100644 --- a/direct/src/plugin/p3dMultifileReader.h +++ b/direct/src/plugin/p3dMultifileReader.h @@ -16,6 +16,7 @@ #define P3DMULTIFILEREADER_H #include "p3d_plugin_common.h" +#include "p3dInstanceManager.h" // for openssl class P3DPackage; @@ -30,13 +31,26 @@ class P3DPackage; class P3DMultifileReader { public: P3DMultifileReader(); + bool open_read(const string &pathname); + inline bool is_open() const; + void close(); - bool extract_all(const string &pathname, const string &to_dir, + bool extract_all(const string &to_dir, P3DPackage *package, double start_progress, double progress_size); - bool extract_one(const string &pathname, ostream &out, - const string &filename); + bool extract_one(ostream &out, const string &filename); + + class CertRecord { + public: + inline CertRecord(X509 *cert); + inline CertRecord(const CertRecord ©); + inline ~CertRecord(); + X509 *_cert; + }; + typedef vector CertChain; + int get_num_signatures() const; + const CertChain &get_signature(int n) const; private: class Subfile; @@ -45,21 +59,39 @@ private: bool read_index(); bool extract_subfile(ostream &out, const Subfile &s); + void check_signatures(); + inline unsigned int read_uint16(); inline unsigned int read_uint32(); - ifstream _in; + enum SubfileFlags { + SF_compressed = 0x0008, + SF_encrypted = 0x0010, + SF_signature = 0x0020, + }; class Subfile { public: + inline size_t get_last_byte_pos() const; + string _filename; - size_t _start; - size_t _length; + size_t _index_start; + size_t _index_length; + size_t _data_start; + size_t _data_length; size_t _timestamp; }; + ifstream _in; + bool _is_open; + typedef vector Subfiles; Subfiles _subfiles; + Subfiles _cert_special; + size_t _last_data_byte; + + typedef vector Certificates; + Certificates _signatures; static const char _header[]; static const size_t _header_size; diff --git a/direct/src/plugin/p3dPackage.cxx b/direct/src/plugin/p3dPackage.cxx index 255ed1e1ea..98b26ea7da 100755 --- a/direct/src/plugin/p3dPackage.cxx +++ b/direct/src/plugin/p3dPackage.cxx @@ -702,8 +702,15 @@ void P3DPackage:: extract_archive() { string source_pathname = _package_dir + "/" + _uncompressed_archive.get_filename(); P3DMultifileReader reader; - if (!reader.extract_all(source_pathname, _package_dir, - this, download_portion + uncompress_portion, extract_portion)) { + if (!reader.open_read(source_pathname)) { + nout << "Couldn't read " << _uncompressed_archive.get_filename() << "\n"; + report_done(false); + return; + } + + if (!reader.extract_all(_package_dir, this, + download_portion + uncompress_portion, + extract_portion)) { nout << "Failure extracting " << _uncompressed_archive.get_filename() << "\n"; report_done(false); diff --git a/direct/src/plugin/p3dSession.cxx b/direct/src/plugin/p3dSession.cxx index 2b10ff53e8..0c77448c22 100644 --- a/direct/src/plugin/p3dSession.cxx +++ b/direct/src/plugin/p3dSession.cxx @@ -661,7 +661,7 @@ start_p3dpython(P3DInstance *inst) { // Change the current directory to the standard start directory, but // only if the runtime environment told us the original current // directory isn't meaningful. - _use_start_dir = !inst_mgr->get_keep_cwd(); + _use_start_dir = !inst_mgr->get_trusted_environment(); if (_use_start_dir) { mkdir_complete(_start_dir, nout); } diff --git a/direct/src/plugin/p3dSplashWindow.cxx b/direct/src/plugin/p3dSplashWindow.cxx index e84ca8dfb4..6c670b0cc1 100755 --- a/direct/src/plugin/p3dSplashWindow.cxx +++ b/direct/src/plugin/p3dSplashWindow.cxx @@ -410,6 +410,7 @@ set_mouse_data(int mouse_x, int mouse_y, bool mouse_down) { _mouse_down = mouse_down; ButtonState bstate = BS_hidden; + bool click_detected = false; if (!_button_active) { // The button isn't active, so it's hidden, regardless of the @@ -439,7 +440,7 @@ set_mouse_data(int mouse_x, int mouse_y, bool mouse_down) { // after a successful click. bstate = BS_hidden; _button_active = false; - button_click_detected(); + click_detected = true; } else { bstate = BS_rollover; } @@ -455,6 +456,13 @@ set_mouse_data(int mouse_x, int mouse_y, bool mouse_down) { } set_bstate(bstate); + + // If we detected a click operation in the above, make the callback + // here, at the end of the method, after we have finished updating + // the button state. + if (click_detected) { + button_click_detected(); + } } //////////////////////////////////////////////////////////////////// diff --git a/direct/src/plugin/p3d_plugin.cxx b/direct/src/plugin/p3d_plugin.cxx index 417cdab080..e4e5c2cd3f 100644 --- a/direct/src/plugin/p3d_plugin.cxx +++ b/direct/src/plugin/p3d_plugin.cxx @@ -38,7 +38,7 @@ P3D_initialize(int api_version, const char *contents_filename, const char *download_url, bool verify_contents, const char *platform, const char *log_directory, const char *log_basename, - bool keep_cwd) { + bool trusted_environment) { if (api_version != P3D_API_VERSION) { // Can't accept an incompatible version. return false; @@ -74,7 +74,7 @@ P3D_initialize(int api_version, const char *contents_filename, bool result = inst_mgr->initialize(contents_filename, download_url, verify_contents, platform, log_directory, log_basename, - keep_cwd); + trusted_environment); RELEASE_LOCK(_api_lock); return result; } diff --git a/direct/src/plugin/p3d_plugin.h b/direct/src/plugin/p3d_plugin.h index a9eadecd94..6f4981aebf 100644 --- a/direct/src/plugin/p3d_plugin.h +++ b/direct/src/plugin/p3d_plugin.h @@ -122,11 +122,11 @@ extern "C" { core API. Note that the individual instances also have their own log_basename values. - Finally, keep_cwd should be set true if the current working - directory is meaningful and valuable to the user (for instance, - when this is launched via a command-line tool), or false if it - means nothing and can safely be reset. Normally, a browser plugin - should set this false. + Finally, trusted_environment should be set true to indicate that + the environment and p3d file are already trusted. If this is set, + the current working directory will remain unchanged, and the p3d + file will be run without checking its signature. Normally, a + browser plugin should set this false. This function returns true if the core API is valid and uses a compatible API, false otherwise. If it returns false, the host @@ -137,7 +137,7 @@ P3D_initialize_func(int api_version, const char *contents_filename, const char *download_url, bool verify_contents, const char *platform, const char *log_directory, const char *log_basename, - bool keep_cwd); + bool trusted_environment); /* This function should be called to unload the core API. It will release all internally-allocated memory and return the core API to