From 078b8b8193d92c13ee80bb8373b44e4b4a91b703 Mon Sep 17 00:00:00 2001 From: David Rose Date: Wed, 2 Oct 2002 00:43:41 +0000 Subject: [PATCH] expected-ssl-server --- panda/src/downloader/httpClient.I | 30 --- panda/src/downloader/httpClient.cxx | 354 +++++++++++++++++++++++++++- panda/src/downloader/httpClient.h | 20 +- 3 files changed, 367 insertions(+), 37 deletions(-) diff --git a/panda/src/downloader/httpClient.I b/panda/src/downloader/httpClient.I index 508d4ee908..f58f8b0016 100644 --- a/panda/src/downloader/httpClient.I +++ b/panda/src/downloader/httpClient.I @@ -17,36 +17,6 @@ //////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////// -// Function: HTTPClient::Constructor -// Access: Published -// Description: -//////////////////////////////////////////////////////////////////// -INLINE HTTPClient:: -HTTPClient() { - make_ctx(); -} - -//////////////////////////////////////////////////////////////////// -// Function: HTTPClient::Copy Constructor -// Access: Published -// Description: -//////////////////////////////////////////////////////////////////// -INLINE HTTPClient:: -HTTPClient(const HTTPClient ©) : _proxy(copy._proxy) { - make_ctx(); -} - -//////////////////////////////////////////////////////////////////// -// Function: HTTPClient::Copy Assignment Operator -// Access: Published -// Description: -//////////////////////////////////////////////////////////////////// -INLINE void HTTPClient:: -operator = (const HTTPClient ©) { - _proxy = copy._proxy; -} - //////////////////////////////////////////////////////////////////// // Function: HTTPClient::set_proxy // Access: Published diff --git a/panda/src/downloader/httpClient.cxx b/panda/src/downloader/httpClient.cxx index 10aa31a0ee..565682b781 100644 --- a/panda/src/downloader/httpClient.cxx +++ b/panda/src/downloader/httpClient.cxx @@ -32,10 +32,55 @@ bool HTTPClient::_ssl_initialized = false; -// Never freed. +// This is created once and never freed. X509_STORE *HTTPClient::_x509_store = NULL; +//////////////////////////////////////////////////////////////////// +// Function: HTTPClient::Constructor +// Access: Published +// Description: +//////////////////////////////////////////////////////////////////// +HTTPClient:: +HTTPClient() { + _verify_ssl = verify_ssl; + make_ctx(); +} + +//////////////////////////////////////////////////////////////////// +// Function: HTTPClient::Copy Constructor +// Access: Published +// Description: +//////////////////////////////////////////////////////////////////// +HTTPClient:: +HTTPClient(const HTTPClient ©) { + _verify_ssl = verify_ssl; + make_ctx(); + + (*this) = copy; +} + +//////////////////////////////////////////////////////////////////// +// Function: HTTPClient::Copy Assignment Operator +// Access: Published +// Description: +//////////////////////////////////////////////////////////////////// +void HTTPClient:: +operator = (const HTTPClient ©) { + _proxy = copy._proxy; + set_verify_ssl(copy._verify_ssl); + clear_expected_servers(); + + ExpectedServers::const_iterator ei; + for (ei = copy._expected_servers.begin(); + ei != copy._expected_servers.end(); + ++ei) { + X509_NAME *orig_name = (*ei); + X509_NAME *new_name = X509_NAME_dup(orig_name); + _expected_servers.push_back(new_name); + } +} + //////////////////////////////////////////////////////////////////// // Function: HTTPClient::Destructor // Access: Published @@ -49,6 +94,9 @@ HTTPClient:: nassertv(_ssl_ctx->cert_store == _x509_store); _ssl_ctx->cert_store = NULL; SSL_CTX_free(_ssl_ctx); + + // Free all of the expected server definitions. + clear_expected_servers(); } //////////////////////////////////////////////////////////////////// @@ -89,6 +137,52 @@ load_certificates(const Filename &filename) { return true; } +//////////////////////////////////////////////////////////////////// +// Function: HTTPClient::add_expected_server +// Access: Published +// Description: Adds the indicated string as a definition of a valid +// server to contact via https. If no servers have been +// been added, an https connection will be allowed to +// any server. If at least one server has been added, +// an https connection will be allowed to any of the +// named servers, but none others. +// +// The string passed in defines a subset of the server +// properties that are to be insisted on, using the X509 +// naming convention, e.g. O=WDI/OU=VRStudio/CN=ttown. +// +// It makes sense to use this in conjunction with +// set_verify_ssl(), which insists that the https +// connection uses a verifiable certificate. +//////////////////////////////////////////////////////////////////// +bool HTTPClient:: +add_expected_server(const string &server_attributes) { + X509_NAME *name = parse_x509_name(server_attributes); + if (name == (X509_NAME *)NULL) { + return false; + } + + _expected_servers.push_back(name); + return true; +} + +//////////////////////////////////////////////////////////////////// +// Function: HTTPClient::clear_expected_servers +// Access: Published +// Description: Clears the set of expected servers; the HTTPClient +// will allow an https connection to any server. +//////////////////////////////////////////////////////////////////// +void HTTPClient:: +clear_expected_servers() { + for (ExpectedServers::iterator ei = _expected_servers.begin(); + ei != _expected_servers.end(); + ++ei) { + X509_NAME *name = (*ei); + X509_NAME_free(name); + } + _expected_servers.clear(); +} + //////////////////////////////////////////////////////////////////// // Function: HTTPClient::get_document // Access: Published @@ -143,11 +237,38 @@ make_ctx() { #endif // Insist on verifying servers if we are configured to. - set_verify_ssl(verify_ssl); + if (_verify_ssl) { + SSL_CTX_set_verify(_ssl_ctx, SSL_VERIFY_PEER, NULL); + } else { + SSL_CTX_set_verify(_ssl_ctx, SSL_VERIFY_NONE, NULL); + } + + // Get the configured set of expected servers. + { + // Load in any default certificates listed in the Configrc file. + Config::ConfigTable::Symbol expected_servers; + config_express.GetAll("expected-ssl-server", expected_servers); + + // When we use GetAll(), we might inadvertently read duplicate + // lines. Filter them out with a set. + pset already_read; + + Config::ConfigTable::Symbol::iterator si; + for (si = expected_servers.begin(); si != expected_servers.end(); ++si) { + string expected_server = (*si).Val(); + if (already_read.insert(expected_server).second) { + add_expected_server(expected_server); + } + } + } if (_x509_store != (X509_STORE *)NULL) { - // If we've already created an x509 store object, use it for this - // context. + // If we've already created an x509 store object, share it with + // this context. It would be better to make a copy of the store + // object for each context, so we could locally add certificates, + // but (a) there doesn't seem to be an interface for this, and (b) + // something funny about loading certificates that seems to save + // some persistent global state anyway. SSL_CTX_set_cert_store(_ssl_ctx, _x509_store); } else { @@ -470,6 +591,7 @@ make_https_connection(BIO *bio, const URLSpec &url) const { #ifndef NDEBUG ERR_print_errors_fp(stderr); #endif + BIO_free_all(bio); return NULL; } @@ -485,6 +607,13 @@ make_https_connection(BIO *bio, const URLSpec &url) const { downloader_cat.info() << "No certificate was presented by server.\n"; + if (!_expected_servers.empty()) { + downloader_cat.info() + << "Not allowing connection since no certificates could be matched.\n"; + BIO_free_all(sbio); + return NULL; + } + } else { if (downloader_cat.is_debug()) { downloader_cat.debug() @@ -492,6 +621,23 @@ make_https_connection(BIO *bio, const URLSpec &url) const { X509_print_fp(stderr, cert); fflush(stderr); } + + X509_NAME *subject = X509_get_subject_name(cert); + + string org_name = get_x509_name_component(subject, NID_organizationName); + string org_unit_name = get_x509_name_component(subject, NID_organizationalUnitName); + string common_name = get_x509_name_component(subject, NID_commonName); + + downloader_cat.info() + << "Server is " << common_name << " from " << org_unit_name + << " of " << org_name << "\n"; + + if (!verify_server(subject)) { + downloader_cat.info() + << "Server does not match any expected server.\n"; + BIO_free_all(sbio); + return NULL; + } X509_free(cert); } @@ -500,6 +646,7 @@ make_https_connection(BIO *bio, const URLSpec &url) const { } + /* Certificate verify error codes: @@ -714,6 +861,205 @@ send_get_request(BIO *bio, BIO_puts(bio, request_str.c_str()); } +//////////////////////////////////////////////////////////////////// +// Function: HTTPClient::verify_server +// Access: Private +// Description: Returns true if the indicated server matches one of +// our expected servers (or the list of expected servers +// is empty), or false if it does not match any of our +// expected servers. +//////////////////////////////////////////////////////////////////// +bool HTTPClient:: +verify_server(X509_NAME *subject) const { + if (_expected_servers.empty()) { + if (downloader_cat.is_debug()) { + downloader_cat.debug() + << "No expected servers on list; allowing any https connection.\n"; + } + return true; + } + + if (downloader_cat.is_debug()) { + downloader_cat.debug() << "checking server: " << flush; + X509_NAME_print_ex_fp(stderr, subject, 0, 0); + fflush(stderr); + downloader_cat.debug(false) << "\n"; + } + + ExpectedServers::const_iterator ei; + for (ei = _expected_servers.begin(); + ei != _expected_servers.end(); + ++ei) { + X509_NAME *expected_name = (*ei); + if (x509_name_subset(expected_name, subject)) { + if (downloader_cat.is_debug()) { + downloader_cat.debug() + << "Match found!\n"; + } + return true; + } + } + + // None of the lines matched. + if (downloader_cat.is_debug()) { + downloader_cat.debug() + << "No match found against any of the following expected servers:\n"; + + for (ei = _expected_servers.begin(); + ei != _expected_servers.end(); + ++ei) { + X509_NAME *expected_name = (*ei); + X509_NAME_print_ex_fp(stderr, expected_name, 0, 0); + fprintf(stderr, "\n"); + } + fflush(stderr); + } + + return false; +} + +//////////////////////////////////////////////////////////////////// +// Function: HTTPClient::parse_x509_name +// Access: Private, Static +// Description: Parses a string of the form +// /type0=value0/type1=value1/type2=... into a newly +// allocated X509_NAME object. Returns NULL if the +// string is invalid. +//////////////////////////////////////////////////////////////////// +X509_NAME *HTTPClient:: +parse_x509_name(const string &source) { + X509_NAME *result = NULL; + + result = X509_NAME_new(); + bool added_any = false; + + string::const_iterator si; + si = source.begin(); + while (si != source.end()) { + if ((*si) == '/') { + // Skip a slash delimiter. + ++si; + } else { + string type; + while (si != source.end() && (*si) != '=' && (*si) != '/') { + if ((*si) == '\\') { + ++si; + if (si != source.end()) { + type += (*si); + ++si; + } + } else { + type += (*si); + ++si; + } + } + + int nid = OBJ_txt2nid(type.c_str()); + if (nid == NID_undef) { + downloader_cat.info() + << "Unknown type " << type << " in X509 name: " << source + << "\n"; + X509_NAME_free(result); + return NULL; + } + + string value; + + if (si != source.end() && (*si) == '=') { + ++si; + while (si != source.end() && (*si) != '/') { + if ((*si) == '\\') { + ++si; + if (si != source.end()) { + value += (*si); + ++si; + } + } else { + value += (*si); + ++si; + } + } + } + + if (!value.empty()) { + int add_result = + X509_NAME_add_entry_by_NID(result, nid, V_ASN1_APP_CHOOSE, + (unsigned char *)value.c_str(), -1, -1, 0); + if (!add_result) { + downloader_cat.info() + << "Unable to add " << type << "=" << value << " in X509 name: " + << source << "\n"; + X509_NAME_free(result); + return NULL; + } + added_any = true; + } + } + } + + if (!added_any) { + downloader_cat.info() + << "Invalid empty X509 name: " << source << "\n"; + X509_NAME_free(result); + return NULL; + } + + return result; +} + +//////////////////////////////////////////////////////////////////// +// Function: HTTPClient::get_x509_name_component +// Access: Private, Static +// Description: Returns the indicated component of the X509 name as a +// string, if defined, or empty string if it is not. +//////////////////////////////////////////////////////////////////// +string HTTPClient:: +get_x509_name_component(X509_NAME *name, int nid) { + ASN1_OBJECT *obj = OBJ_nid2obj(nid); + + if (obj == NULL) { + // Unknown nid. See openssl/objects.h. + return string(); + } + + int i = X509_NAME_get_index_by_OBJ(name, obj, -1); + if (i < 0) { + return string(); + } + + ASN1_STRING *data = X509_NAME_ENTRY_get_data(X509_NAME_get_entry(name, i)); + return string((char *)data->data, data->length); +} + +//////////////////////////////////////////////////////////////////// +// Function: HTTPClient::x509_name_subset +// Access: Private, Static +// Description: Returns true if name_a is a subset of name_b: each +// property of name_a is defined in name_b, and the +// defined value is equivalent to that of name_a. +//////////////////////////////////////////////////////////////////// +bool HTTPClient:: +x509_name_subset(X509_NAME *name_a, X509_NAME *name_b) { + int count_a = X509_NAME_entry_count(name_a); + for (int ai = 0; ai < count_a; ai++) { + X509_NAME_ENTRY *na = X509_NAME_get_entry(name_a, ai); + + int bi = X509_NAME_get_index_by_OBJ(name_b, na->object, -1); + if (bi < 0) { + // This entry in name_a is not defined in name_b. + return false; + } + + X509_NAME_ENTRY *nb = X509_NAME_get_entry(name_b, bi); + if (na->value->length != nb->value->length || + memcmp(na->value->data, nb->value->data, na->value->length) != 0) { + // This entry in name_a doesn't match that of name_b. + return false; + } + } + return true; +} + #ifndef NDEBUG //////////////////////////////////////////////////////////////////// // Function: HTTPClient::show_send diff --git a/panda/src/downloader/httpClient.h b/panda/src/downloader/httpClient.h index ab819467b9..2a625ecd03 100644 --- a/panda/src/downloader/httpClient.h +++ b/panda/src/downloader/httpClient.h @@ -47,9 +47,9 @@ class Filename; //////////////////////////////////////////////////////////////////// class EXPCL_PANDAEXPRESS HTTPClient { PUBLISHED: - INLINE HTTPClient(); - INLINE HTTPClient(const HTTPClient ©); - INLINE void operator = (const HTTPClient ©); + HTTPClient(); + HTTPClient(const HTTPClient ©); + void operator = (const HTTPClient ©); ~HTTPClient(); INLINE void set_proxy(const URLSpec &proxy); @@ -60,6 +60,9 @@ PUBLISHED: INLINE void set_verify_ssl(bool verify_ssl); INLINE bool get_verify_ssl() const; + bool add_expected_server(const string &server_attributes); + void clear_expected_servers(); + PT(HTTPDocument) get_document(const URLSpec &url, const string &body = string()); private: @@ -76,6 +79,11 @@ private: void send_get_request(BIO *bio, const string &path, const string &server, const string &body) const; + bool verify_server(X509_NAME *subject) const; + + static X509_NAME *parse_x509_name(const string &source); + static string get_x509_name_component(X509_NAME *name, int nid); + static bool x509_name_subset(X509_NAME *name_a, X509_NAME *name_b); #ifndef NDEBUG static void show_send(const string &message); @@ -89,6 +97,12 @@ private: URLSpec _proxy; bool _verify_ssl; + + // List of allowable SSL servers to connect to. If the list is + // empty, any server is acceptable. + typedef pvector ExpectedServers; + ExpectedServers _expected_servers; + SSL_CTX *_ssl_ctx; static bool _ssl_initialized;