-rw-r--r-- | include/Makefile.am | 21 | ||||
-rw-r--r-- | include/opkele/basic_rp.h | 218 | ||||
-rw-r--r-- | include/opkele/discovery.h | 88 | ||||
-rw-r--r-- | include/opkele/exception.h | 19 | ||||
-rw-r--r-- | include/opkele/extension.h | 7 | ||||
-rw-r--r-- | include/opkele/extension_chain.h | 6 | ||||
-rw-r--r-- | include/opkele/prequeue_rp.h | 81 | ||||
-rw-r--r-- | include/opkele/sreg.h | 8 | ||||
-rw-r--r-- | include/opkele/types.h | 171 | ||||
-rw-r--r-- | include/opkele/util.h | 8 | ||||
-rw-r--r-- | lib/Makefile.am | 7 | ||||
-rw-r--r-- | lib/basic_rp.cc | 311 | ||||
-rw-r--r-- | lib/consumer.cc | 4 | ||||
-rw-r--r-- | lib/discovery.cc | 161 | ||||
-rw-r--r-- | lib/extension.cc | 6 | ||||
-rw-r--r-- | lib/extension_chain.cc | 12 | ||||
-rw-r--r-- | lib/openid_message.cc | 228 | ||||
-rw-r--r-- | lib/params.cc | 101 | ||||
-rw-r--r-- | lib/prequeue_rp.cc | 81 | ||||
-rw-r--r-- | lib/server.cc | 2 | ||||
-rw-r--r-- | lib/sreg.cc | 54 | ||||
-rw-r--r-- | lib/util.cc | 71 |
22 files changed, 1366 insertions, 299 deletions
diff --git a/include/Makefile.am b/include/Makefile.am index 51dcea1..50fcb62 100644 --- a/include/Makefile.am +++ b/include/Makefile.am @@ -1,21 +1,32 @@ -nobase_include_HEADERS = \ +NODIST_HEADERS_ = \ opkele/acconfig.h \ + opkele/tr1-mem.h + +nobase_include_HEADERS = \ opkele/opkele-config.h \ opkele/types.h \ opkele/association.h \ opkele/exception.h \ opkele/server.h \ opkele/consumer.h \ opkele/extension.h \ opkele/sreg.h \ opkele/extension_chain.h \ opkele/xconsumer.h \ opkele/xserver.h \ - opkele/discovery.h \ opkele/uris.h \ - opkele/tr1-mem.h -EXTRA_DIST = \ + opkele/tr1-mem.h \ + opkele/basic_rp.h \ + opkele/prequeue_rp.h \ + opkele/iterator.h \ + ${NODIST_HEADERS_} + +noinst_HEADERS = \ opkele/data.h \ opkele/curl.h opkele/expat.h opkele/tidy.h \ opkele/util.h \ - opkele/debug.h + opkele/debug.h \ + opkele/discovery.h + +dist-hook: + rm -f $(addprefix ${distdir}/,${NODIST_HEADERS_}) diff --git a/include/opkele/basic_rp.h b/include/opkele/basic_rp.h new file mode 100644 index 0000000..3f17fd9 --- a/dev/null +++ b/include/opkele/basic_rp.h @@ -0,0 +1,218 @@ +#ifndef __OPKELE_BASIC_RP_H +#define __OPKELE_BASIC_RP_H + +#include <cstring> +#include <string> +#include <opkele/types.h> +#include <opkele/extension.h> + +namespace opkele { + using std::string; + + struct openid_endpoint_t { + string uri; + string claimed_id; + string local_id; + + openid_endpoint_t() { } + openid_endpoint_t(const string& u,const string& cid,const string& lid) + : uri(u), claimed_id(cid), local_id(lid) { } + + bool operator==(const openid_endpoint_t& x) const { + return uri==x.uri && local_id==x.local_id; } + bool operator<(const openid_endpoint_t& x) const { + int c; + return (c=strcmp(uri.c_str(),x.uri.c_str())) + ? (c<0) : (strcmp(local_id.c_str(),x.local_id.c_str())<0); } + }; + + class basic_RP { + public: + + virtual ~basic_RP() { } + + /** + * @name Global persistent store API + * These are functions related to the associations with OP storage + * and retrieval and nonce records. They provide an interface to + * the persistent storage which is shared by all sessions. If the + * implementor prefers the dumb mode instead, the function should + * throw dumb_RP exception instead. + * @see opkele::dumb_RP + * @{ + */ + /** + * Store association and return allocated association object. + * @param OP OP endpoint + * @param handle association handle + * @param type association type + * @param secret association secret + * @params expires_in the number of seconds association expires in + * @return the association object + * @throw dumb_RP for dumb RP + */ + virtual assoc_t store_assoc( + const string& OP,const string& handle, + const string& type,const secret_t& secret, + int expires_in) = 0; + /** + * Find valid unexpired association with an OP. + * @param OP OP endpoint URL + * @return association found + * @throw failed_lookup if no association found + * @throw dumb_RP for dumb RP + */ + virtual assoc_t find_assoc( + const string& OP) = 0; + /** + * Retrieve valid association handle for an OP by handle. + * @param OP OP endpoint URL + * @param handle association handle + * @return association found + * @throw failed_lookup if no association found + * @throw dumb_RP for dumb RP + */ + virtual assoc_t retrieve_assoc( + const string& OP,const string& handle) = 0; + /** + * Invalidate association with OP + * @param OP OP endpoint URL + * @param handle association handle + * @throw dumb_RP for dumb RP + */ + virtual void invalidate_assoc(const string& OP,const string& handle) = 0; + + /** + * Check the nonce validity. That is, check that we haven't + * accepted request with this nonce from this OP, yet. May involve + * cutting off by the timestamp and checking the rest against the + * store of seen nonces. + * @param OP OP endpoint URL + * @param nonce nonce value + * @throw id_res_bad_nonce if the nonce is not to be accepted, i.e. + * either too old or seen. + */ + virtual void check_nonce(const string& OP,const string& nonce) = 0; + /** + * @} + */ + + /** + * @name Session persistent store API + * @{ + */ + /** + * Retrieve OpenID endpoint being currently used for + * authentication. If there is no endpoint available, throw a + * no_endpoint exception. + * @return reference to the service endpoint object + * @see next_endpoint + * @throw no_endpoint if no endpoint available + */ + virtual const openid_endpoint_t& get_endpoint() const = 0; + /** + * Advance to the next endpoint to try. + * @see get_endpoint() + * @throw no_endpoint if there are no more endpoints + */ + virtual void next_endpoint() = 0; + /** + * @} + */ + + /** + * @name Site particulars API + * @{ + */ + /** + * Return an absolute URL of the page being processed, includining + * query parameters. It is used to validate return_to URL on + * positive assertions. + * @return fully qualified url of the page being processed. + */ + virtual const string get_this_url() const = 0; + /** + * @} + */ + + /** + * @name OpenID actions + * @{ + */ + /** + * Initiates authentication session, doing discovery, normalization + * and whatever implementor wants to do at this point. + * @param usi User-supplied identity + */ + virtual void initiate(const string& usi) = 0; + /** + * Prepare checkid_request. + * @param rv reference to the openid message to prepare + * @param mode checkid_setup or checkid_immediate + * @param return_to the URL OP should redirect to after completion + * @param realm authentication realm to pass to OP + * @param ext pointer to extension to use in request preparation + * @return reference to the openid message + */ + basic_openid_message& checkid_( + basic_openid_message& rv, + mode_t mode, + const string& return_to,const string& realm, + extension_t *ext=0); + /** + * Verify assertion at the end of round-trip. + * @param om incoming openid message + * @param ext pointer to extention to use in parsing assertion + * @throw id_res_setup if checkid_immediate request could not be + * completed + * @throw id_res_cancel if authentication request was canceled + * @throw id_res_mismatch in case of signature mismatch + * @throw id_res_bad_return_to if return_to url seems to be + * tampered with + * @throw id_res_unauthorized if OP is not authorized to make + * assertions regarding the identity + */ + void id_res(const basic_openid_message& om,extension_t *ext=0); + + /** + * Establish association with OP + * @param OP OP to establish association with + * @throw dumb_RP if for a dumb RP + */ + virtual assoc_t associate(const string& OP); + /** + * Check authentication with OP and invalidate handle if requested + * and confirmed + * @param OP OP to check with + * @param om message to check + * @throw failed_check_authentication if OP fails to confirm + * authenticity of the assertion + */ + void check_authentication(const string& OP,const basic_openid_message& om); + /** + * @} + */ + + /** + * @name Miscellanea + * @{ + */ + /** + * Verify OP authority. Return normally if OP is authorized to make + * an assertion, throw an exception otherwise. + * @param OP OP endpoint + * @param claimed_id claimed identity + * @param identity OP-Local identifier + * @throw id_res_unauthorized if OP is not authorized to make + * assertion regarding this identity. + */ + virtual void verify_OP(const string& OP, + const string& claimed_id,const string& identity) const = 0; + /** + * @} + */ + }; + +} + +#endif /* __OPKELE_BASIC_RP_H */ diff --git a/include/opkele/discovery.h b/include/opkele/discovery.h index af4aa29..ab4b9d9 100644 --- a/include/opkele/discovery.h +++ b/include/opkele/discovery.h @@ -1,40 +1,104 @@ #ifndef __OPKELE_DISCOVERY_H #define __OPKELE_DISCOVERY_H #include <string> #include <opkele/types.h> +#include <opkele/basic_rp.h> namespace opkele { using std::string; - struct idiscovery_t; + namespace xrd { - void idiscover(idiscovery_t& result,const string& identity); + struct priority_compare { + inline bool operator()(long a,long b) const { + return (a<0) ? false : (b<0) ? true : (a<b); + } + }; + + template <typename _DT> + class priority_map : public multimap<long,_DT,priority_compare> { + typedef multimap<long,_DT,priority_compare> map_type; + public: + + inline _DT& add(long priority,const _DT& d) { + return insert(typename map_type::value_type(priority,d))->second; + } + + bool has_value(const _DT& d) const { + for(typename map_type::const_iterator i=this->begin();i!=this->end();++i) + if(i->second==d) return true; + return false; + } + }; + + typedef priority_map<string> canonical_ids_t; + typedef priority_map<string> local_ids_t; + typedef set<string> types_t; + typedef priority_map<string> uris_t; + + class service_t { + public: + types_t types; + uris_t uris; + local_ids_t local_ids; + string provider_id; + + void clear() { + types.clear(); + uris.clear(); local_ids.clear(); + provider_id.clear(); + } + }; + typedef priority_map<service_t> services_t; + + class XRD_t { + public: + time_t expires; + + canonical_ids_t canonical_ids; + local_ids_t local_ids; + services_t services; + string provider_id; + + void clear() { + expires = 0; + canonical_ids.clear(); local_ids.clear(); + services.clear(); + provider_id.clear(); + } + bool empty() const { + return + canonical_ids.empty() + && local_ids.empty() + && services.empty(); + } + + }; + + } + + typedef util::output_iterator_proxy<openid_endpoint_t> + endpoint_discovery_iterator; + + string idiscover( + endpoint_discovery_iterator oi, + const string& identity); struct idiscovery_t { bool xri_identity; string normalized_id; string canonicalized_id; xrd::XRD_t xrd; idiscovery_t() { } - idiscovery_t(const string& i) { - idiscover(*this,i); - } - idiscovery_t(const char *i) { - idiscover(*this,i); - } void clear() { normalized_id.clear(); canonicalized_id.clear(); xrd.clear(); } - idiscovery_t& operator=(const string& i) { - idiscover(*this,i); return *this; } - idiscovery_t& operator=(const char *i) { - idiscover(*this,i); return *this; } }; } #endif /* __OPKELE_DISCOVERY_H */ diff --git a/include/opkele/exception.h b/include/opkele/exception.h index a8c3339..ccb39d9 100644 --- a/include/opkele/exception.h +++ b/include/opkele/exception.h @@ -122,195 +122,214 @@ namespace opkele { /** * thrown if the handle being retrieved is invalid */ class invalid_handle : public exception { public: invalid_handle(OPKELE_E_PARS) : exception(OPKELE_E_CONS) { } }; /** * thrown if the handle passed to check_authentication request is not * stateless */ class stateful_handle : public exception { public: stateful_handle(OPKELE_E_PARS) : exception(OPKELE_E_CONS) { } }; /** * thrown if check_authentication request fails */ class failed_check_authentication : public exception { public: failed_check_authentication(OPKELE_E_PARS) : exception(OPKELE_E_CONS) { } }; /** * thrown if the id_res request result is negative */ class id_res_failed : public exception { public: id_res_failed(OPKELE_E_PARS) : exception(OPKELE_E_CONS) { } }; /** * thrown if the user_setup_url is provided with negative response */ class id_res_setup : public id_res_failed { public: string setup_url; id_res_setup(OPKELE_E_PARS,const string& su="") : id_res_failed(OPKELE_E_CONS), setup_url(su) { } ~id_res_setup() throw() { } }; /** * thrown in case of signature mismatch */ class id_res_mismatch : public id_res_failed { public: id_res_mismatch(OPKELE_E_PARS) : id_res_failed(OPKELE_E_CONS) { } }; /** * thrown if the association has expired before it could've been verified. */ class id_res_expired_on_delivery : public id_res_failed { public: id_res_expired_on_delivery(OPKELE_E_PARS) : id_res_failed(OPKELE_E_CONS) { } }; /** * thown when the user cancelled authentication process. */ class id_res_cancel : public id_res_failed { public: id_res_cancel(OPKELE_E_PARS) : id_res_failed(OPKELE_E_CONS) { } }; /** * thrown in case of nonce reuse or otherwise imperfect nonce. */ class id_res_bad_nonce : public id_res_failed { public: id_res_bad_nonce(OPKELE_E_PARS) : id_res_failed(OPKELE_E_CONS) { } }; /** * thrown if return_to didn't pass verification */ class id_res_bad_return_to : public id_res_failed { public: id_res_bad_return_to(OPKELE_E_PARS) : id_res_failed(OPKELE_E_CONS) { } }; /** * thrown if OP isn't authorized to make an assertion */ class id_res_unauthorized : public id_res_failed { public: id_res_unauthorized(OPKELE_E_PARS) : id_res_failed(OPKELE_E_CONS) { } }; /** * openssl malfunction occured */ class exception_openssl : public exception { public: unsigned long _error; string _ssl_string; exception_openssl(OPKELE_E_PARS); ~exception_openssl() throw() { } }; /** * network operation related error occured */ class exception_network : public exception { public: exception_network(OPKELE_E_PARS) : exception(OPKELE_E_CONS) { } }; /** * network operation related error occured, specifically, related to * libcurl */ class exception_curl : public exception_network { public: CURLcode _error; string _curl_string; exception_curl(OPKELE_E_PARS); exception_curl(OPKELE_E_PARS,CURLcode e); ~exception_curl() throw() { } }; /** * htmltidy related error occured */ class exception_tidy : public exception { public: int _rc; exception_tidy(OPKELE_E_PARS); exception_tidy(OPKELE_E_PARS,int r); ~exception_tidy() throw() { } }; /** * exception thrown in case of failed discovery */ class failed_discovery : public exception { public: failed_discovery(OPKELE_E_PARS) : exception(OPKELE_E_CONS) { } }; /** * unsuccessfull xri resolution */ class failed_xri_resolution : public failed_discovery { public: long _code; failed_xri_resolution(OPKELE_E_PARS,long _c=-1) : failed_discovery(OPKELE_E_CONS), _code(_c) { } }; /** * not implemented (think pure virtual) member function executed, signfies * programmer error */ class not_implemented : public exception { public: not_implemented(OPKELE_E_PARS) : exception(OPKELE_E_CONS) { } }; /** * internal error, indicates internal libopkele problem */ class internal_error : public exception { public: internal_error(OPKELE_E_PARS) : exception(OPKELE_E_CONS) { } }; /** * thrown in case of unsupported parameter encountered (e.g. unsupported * association type). */ class unsupported : public exception { public: unsupported(OPKELE_E_PARS) : exception(OPKELE_E_CONS) { } }; + /** + * thrown by associations store related functions in case of dumb RP. + */ + class dumb_RP : public exception { + public: + dumb_RP(OPKELE_E_PARS) + : exception(OPKELE_E_CONS) { } + }; + + /** + * thrown by endpoint-queue related function if endpoint is being + * accessed but there's no endpoint available. + */ + class no_endpoint : public exception { + public: + no_endpoint(OPKELE_E_PARS) + : exception(OPKELE_E_CONS) { } + }; + } #endif /* __OPKELE_EXCEPTION_H */ diff --git a/include/opkele/extension.h b/include/opkele/extension.h index 513672f..3ee25ee 100644 --- a/include/opkele/extension.h +++ b/include/opkele/extension.h @@ -1,65 +1,66 @@ #ifndef __OPKELE_EXTENSION_H #define __OPKELE_EXTENSION_H /** * @file * @brief extensions framework basics */ #include <opkele/types.h> namespace opkele { /** * OpenID extension hooks base class */ class extension_t { public: virtual ~extension_t() { } + /** * hook called by consumer before submitting data to OpenID server. * It is supposed to manipulate parameters list. * @param p parameters about to be submitted to server * @param identity identity being verified. It may differ from the * one available in parameters list in case of delegation * @see consumer_t::checkid_ * @see consumer_t::checkid_immediate * @see consumer_t::checkid_setup */ - virtual void checkid_hook(params_t& p,const string& identity); + virtual void checkid_hook(basic_openid_message& om); /** * hook called by consumer after identity information received from * OpenID server is verified. * @param p parameters received from server * @param sp signed parameters received from server with 'openid.' * leader stripped * @param identity identity confirmed. May differ from the one * available in parameters list in case of delegation. May also be * empty which means - extract one from parameters * @see consumer_t::id_res */ - virtual void id_res_hook(const params_t& p,const params_t& sp,const string& identity); + virtual void id_res_hook(const basic_openid_message& om,const basic_openid_message& sp); /** * hook called by server before returning information to consumer. * The hook may manipulate output parameters. It is important to * note that modified pout["signed"] is used for signing response. * @param pin request parameters list with "openid." prefix * @param pout response parameters list without "openid." prefix * @see server_t::checkid_ * @see server_t::checkid_immediate * @see server_t::checkid_setup */ - virtual void checkid_hook(const params_t& pin,params_t& pout); + virtual void checkid_hook(const basic_openid_message& inm,basic_openid_message& oum); /** * Casts the object to pointer to itself. For convenient passing * of pointer. */ operator extension_t*(void) { return this; } }; } #endif /* __OPKELE_EXTENSION_H */ diff --git a/include/opkele/extension_chain.h b/include/opkele/extension_chain.h index f0eea94..fb9bc84 100644 --- a/include/opkele/extension_chain.h +++ b/include/opkele/extension_chain.h @@ -1,38 +1,38 @@ #ifndef __OPKELE_EXTENSION_CHAIN_H #define __OPKELE_EXTENSION_CHAIN_H /** * @file * @brief extension chain extension */ #include <list> #include <opkele/extension.h> namespace opkele { using std::list; /** * OpenID extensions chain used to combine extensions, it is actually an * stl list of pointers to extensions. */ class extension_chain_t : public extension_t, public list<extension_t*> { public: /** * Default constructor creates an empty chain */ extension_chain_t() { } /** * Create extension chain with a single extension in it */ extension_chain_t(extension_t *e) { push_back(e); } - virtual void checkid_hook(params_t& p,const string& identity); - virtual void id_res_hook(const params_t& p,const params_t& sp,const string& identity); - virtual void checkid_hook(const params_t& pin,params_t& pout); + virtual void checkid_hook(basic_openid_message& om); + virtual void id_res_hook(const basic_openid_message& om,const basic_openid_message& sp); + virtual void checkid_hook(const basic_openid_message& inm,basic_openid_message& oum); }; } #endif /* __OPKELE_EXTENSION_CHAIN_H */ diff --git a/include/opkele/prequeue_rp.h b/include/opkele/prequeue_rp.h new file mode 100644 index 0000000..b98dd5a --- a/dev/null +++ b/include/opkele/prequeue_rp.h @@ -0,0 +1,81 @@ +#ifndef __OPKELE_RP_H +#define __OPKELE_RP_H + +#include <string> +#include <set> +#include <iterator> +#include <opkele/basic_rp.h> + +namespace opkele { + using std::string; + using std::set; + using std::iterator; + using std::output_iterator_tag; + + class prequeue_RP : public basic_RP { + public: + /** + * @name Session persistent store API + * @{ + */ + /** + * Called before queueing discovered endpoints. Typically happens + * while initiating authentication session. + * @see queue_endpoint() + * @see end_queueing() + */ + virtual void begin_queueing() { } + /** + * Used to queue discovered endpoint. It is implementors + * responsibility to store the endpoint wherever he choses to store + * it. + * @param oep the endpoint to queue + * @see begin_queueing() + * @see end_queueing() + */ + virtual void queue_endpoint(const openid_endpoint_t& oep) = 0; + /** + * Called after all discovered endpoints were queued. Implementor + * may chose to use this virtual to commit endpoints queue to + * persistent store. + * @see begin_queueing() + * @see queue_endpoint() + */ + virtual void end_queueing() { } + + /** + * Used to store normalized id when initiating request. + * The default implementation does nothing, because implementor + * doesn't have to care. + * @param nid normalized id + * @see get_normalzied_id() + */ + virtual void set_normalized_id(const string& nid); + /** + * Return the normalized id previously set by set_normalized_id(). + * Provided for the sake of completeness because default + * implementation doesn't use it. + * @return the normalized identity + */ + virtual const string get_normalized_id() const; + /** + * @} + */ + + /** + * @name Actions + * @{ + */ + void initiate(const string& usi); + + /** + * @} + */ + + void verify_OP(const string& OP, + const string& claimed_id,const string& identity) const; + }; + +} + +#endif /* __OPKELE_RP_H */ diff --git a/include/opkele/sreg.h b/include/opkele/sreg.h index df37a86..24cb315 100644 --- a/include/opkele/sreg.h +++ b/include/opkele/sreg.h @@ -1,203 +1,203 @@ #ifndef __OPKELE_SREG_H #define __OPKELE_SREG_H /** * @file * @brief Simple registration extension */ #include <opkele/extension.h> namespace opkele { using std::map; /** * OpenID simple registration extension implementation * http://openid.net/specs/openid-simple-registration-extension-1_0.html */ class sreg_t : public extension_t { public: /** * sreg fields enumeration */ enum fieldbit_t { /** * Any UTF-8 string that the End User wants to use as a nickname. */ field_nickname = 1, /** * The email address of the End User as specified in section 3.4.1 of [RFC2822] */ field_email = 2, /** * UTF-8 string free text representation of the End User's full name. */ field_fullname = 4, /** * The End User's date of birth as YYYY-MM-DD. Any values whose * representation uses fewer than the specified number of * digits should be zero-padded. The length of this value MUST * always be 10. If the End User user does not want to reveal * any particular component of this value, it MUST be set to * zero. * * For instance, if a End User wants to specify that his date * of birth is in 1980, but not the month or day, the value * returned SHALL be "1980-00-00". */ field_dob = 8, /** * Alias to field_dob */ field_birthdate = field_dob, /** * The End User's gender, "M" for male, "F" for female. */ field_gender = 16, /** * Alias to field_gender */ field_sex = field_gender, /** * UTF-8 string free text that SHOULD conform to the End User's * country's postal system. */ field_postcode = 32, /** * The End User's country of residence as specified by ISO3166 */ field_country = 64, /** * End User's preferred language as specified by ISO639 */ field_language = 128, /** * ASCII string from TimeZone database * * For example, "Europe/Paris" or "America/Los_Angeles". */ field_timezone = 256, /** * All fields bits combined */ fields_ALL = 511, /** * No fields */ fields_NONE = 0 }; /** * Bitmask for fields which, if absent from the response, will * prevent the Consumer from completing the registration without * End User interation. */ long fields_required; /** * Bitmask for fields that will be used by the Consumer, but whose * absence will not prevent the registration from completing. */ long fields_optional; /** * A URL which the Consumer provides to give the End User a place * to read about the how the profile data will be used. The * Identity Provider SHOULD display this URL to the End User if it * is given. */ string policy_url; /** * Bitmask for fields present in response */ long has_fields; /** * Container type for response fields values */ typedef map<fieldbit_t,string> response_t; /** * Response contents */ response_t response; /** * Fields bitmask to send in response */ long fields_response; /** * Consumer constructor. * @param fr required fields * @see fields_required * @param fo optional fields * @see fields_optional * @param pu policy url * @see policy_url */ sreg_t(long fr=fields_NONE,long fo=fields_NONE,const string& pu="") : fields_required(fr), fields_optional(fo), policy_url(pu), has_fields(0) { } /** * Implementation of consumer's checkid hook */ - virtual void checkid_hook(params_t& p,const string& identity); + virtual void checkid_hook(basic_openid_message& om); /** * Implementation of consumer's id_res hook */ - virtual void id_res_hook(const params_t& p,const params_t& sp,const string& identity); + virtual void id_res_hook(const basic_openid_message& om,const basic_openid_message& sp); /** * Implementation of server's checkid_hook */ - virtual void checkid_hook(const params_t& pin,params_t& pout); + virtual void checkid_hook(const basic_openid_message& inm,basic_openid_message& oum); /** * Check and see if we have value for some particular field. * @param fb field in question * @see fieldbit_t * @return true if the value is available */ bool has_field(fieldbit_t fb) const { return has_fields&fb; } /** * Retrieve the value for a field. * @param fb field in question * @see fieldbit_t * @return field value * @throw failed_lookup if no data avaialble */ const string& get_field(fieldbit_t fb) const; /** * Set the value for a field. * @param fb field in question * @see fieldbit_t * @param fv field value */ void set_field(fieldbit_t fb,const string& fv); /** * Remove the value for a field. * @param fb field in question * @see fieldbit_t */ void reset_field(fieldbit_t fb); /** * Reset field data */ void clear(); /** * Function called after parsing sreg request to set up response * fields. The default implementation tries to send as much fields * as we have. The function is supposed to set the data and * fields_response. * @see fields_response * @param pin input request parameters with "openid." prefix * @param pout output request parameters without "openid." prefix. * @see checkid_hook(const params_t&,params_t&) */ - virtual void setup_response(const params_t& pin,params_t& pout); + virtual void setup_response(const basic_openid_message& inm,basic_openid_message& oum); }; } #endif /* __OPKELE_SREG_H */ diff --git a/include/opkele/types.h b/include/opkele/types.h index de44a5c..d5ad258 100644 --- a/include/opkele/types.h +++ b/include/opkele/types.h @@ -1,246 +1,207 @@ #ifndef __OPKELE_TYPES_H #define __OPKELE_TYPES_H /** * @file * @brief various types declarations */ #include <ostream> #include <vector> #include <string> #include <map> #include <set> +#include <list> +#include <opkele/iterator.h> #include <opkele/tr1-mem.h> namespace opkele { using std::vector; using std::string; using std::map; using std::ostream; using std::multimap; using std::set; + using std::list; + using std::iterator; + using std::forward_iterator_tag; /** * the OpenID operation mode */ typedef enum _mode_t { mode_associate, mode_checkid_immediate, mode_checkid_setup, mode_check_association } mode_t; /** * the association secret container */ class secret_t : public vector<unsigned char> { public: /** * xor the secret and hmac together and encode, using base64 * @param key_d pointer to the message digest * @param rv reference to the return value */ void enxor_to_base64(const unsigned char *key_d,string& rv) const; /** * decode base64-encoded secret and xor it with the message digest * @param key_d pointer to the message digest * @param b64 base64-encoded secret value */ void enxor_from_base64(const unsigned char *key_d,const string& b64); /** * plainly encode to base64 representation * @param rv reference to the return value */ void to_base64(string& rv) const; /** * decode cleartext secret from base64 * @param b64 base64-encoded representation of the secret value */ void from_base64(const string& b64); }; /** * Interface to the association. */ class association_t { public: virtual ~association_t() { } /** * retrieve the server with which association was established. * @return server name */ virtual string server() const = 0; /** * retrieve the association handle. * @return handle */ virtual string handle() const = 0; /** * retrieve the association type. * @return association type */ virtual string assoc_type() const = 0; /** * retrieve the association secret. * @return association secret */ virtual secret_t secret() const = 0; /** * retrieve the number of seconds the association expires in. * @return seconds till expiration */ virtual int expires_in() const = 0; /** * check whether the association is stateless. * @return true if stateless */ virtual bool stateless() const = 0; /** * check whether the association is expired. * @return true if expired */ virtual bool is_expired() const = 0; }; /** * the shared_ptr<> for association_t object type */ typedef tr1mem::shared_ptr<association_t> assoc_t; + class basic_openid_message { + public: + typedef list<string> fields_t; + typedef util::forward_iterator_proxy< + string,const string&,const string* + > fields_iterator; + + basic_openid_message() { } + basic_openid_message(const basic_openid_message& x); + void copy_to(basic_openid_message& x) const; + + virtual bool has_field(const string& n) const = 0; + virtual const string& get_field(const string& n) const = 0; + + virtual bool has_ns(const string& uri) const; + virtual string get_ns(const string& uri) const; + + virtual fields_iterator fields_begin() const = 0; + virtual fields_iterator fields_end() const = 0; + + virtual string append_query(const string& url) const; + virtual string query_string() const; + + + virtual void reset_fields(); + virtual void set_field(const string& n,const string& v); + virtual void reset_field(const string& n); + + virtual void from_keyvalues(const string& kv); + + void add_to_signed(const string& fields); + string find_ns(const string& uri,const char *pfx) const; + string allocate_ns(const string& uri,const char *pfx); + }; + + class openid_message_t : public basic_openid_message, public map<string,string> { + public: + openid_message_t() { } + openid_message_t(const basic_openid_message& x) + : basic_openid_message(x) { } + + void copy_to(basic_openid_message& x) const; + + bool has_field(const string& n) const; + const string& get_field(const string& n) const; + virtual fields_iterator fields_begin() const; + virtual fields_iterator fields_end() const; + + void reset_fields(); + void set_field(const string& n,const string& v); + void reset_field(const string& n); + }; + /** * request/response parameters map */ - class params_t : public map<string,string> { + class params_t : public openid_message_t { public: /** * check whether the parameter is present. * @param n the parameter name * @return true if yes */ - bool has_param(const string& n) const; + bool has_param(const string& n) const { + return has_field(n); } /** * retrieve the parameter (const version) * @param n the parameter name * @return the parameter value * @throw failed_lookup if there is no such parameter */ - const string& get_param(const string& n) const; - /** - * retrieve the parameter. - * @param n the parameter name - * @return the parameter value - * @throw failed_lookup if there is no such parameter - */ - string& get_param(const string& n); + const string& get_param(const string& n) const { + return get_field(n); } /** * parse the OpenID key/value data. * @param kv the OpenID key/value data */ - void parse_keyvalues(const string& kv); - /** - * sign the fields. - * @param secret the secret used for signing - * @param sig reference to the string, containing base64-encoded - * result - * @param slist the comma-separated list of fields to sign - * @param prefix the string to prepend to parameter names - */ - void sign(secret_t secret,string& sig,const string& slist,const char *prefix=0) const; + void parse_keyvalues(const string& kv) { + from_keyvalues(kv); } - /** - * append parameters to the URL as a GET-request parameters. - * @param url the base URL - * @param prefix the string to prepend to parameter names - * @return the ready-to-use location - */ - string append_query(const string& url,const char *prefix = "openid.") const; + string append_query(const string& url,const char *prefix="openid.") const; - /** - * make up a query string suitable for use in GET and POST - * requests. - * @param prefix string to prened to parameter names - * @return query string - */ - string query_string(const char *prefix = "openid.") const; }; - /** - * dump the key/value pairs for the parameters to the stream. - * @param o output stream - * @param p the parameters - */ - ostream& operator << (ostream& o,const params_t& p); - - namespace xrd { - - struct priority_compare { - inline bool operator()(long a,long b) const { - return (a<0) ? false : (b<0) ? true : (a<b); - } - }; - - template <typename _DT> - class priority_map : public multimap<long,_DT,priority_compare> { - typedef multimap<long,_DT,priority_compare> map_type; - public: - - inline _DT& add(long priority,const _DT& d) { - return insert(typename map_type::value_type(priority,d))->second; - } - }; - - typedef priority_map<string> canonical_ids_t; - typedef priority_map<string> local_ids_t; - typedef set<string> types_t; - typedef priority_map<string> uris_t; - - class service_t { - public: - types_t types; - uris_t uris; - local_ids_t local_ids; - string provider_id; - - void clear() { - types.clear(); - uris.clear(); local_ids.clear(); - provider_id.clear(); - } - }; - typedef priority_map<service_t> services_t; - - class XRD_t { - public: - time_t expires; - - canonical_ids_t canonical_ids; - local_ids_t local_ids; - services_t services; - string provider_id; - - void clear() { - expires = 0; - canonical_ids.clear(); local_ids.clear(); - services.clear(); - provider_id.clear(); - } - bool empty() const { - return - canonical_ids.empty() - && local_ids.empty() - && services.empty(); - } - - }; - - } - } #endif /* __OPKELE_TYPES_H */ diff --git a/include/opkele/util.h b/include/opkele/util.h index 085c9e6..e9176b0 100644 --- a/include/opkele/util.h +++ b/include/opkele/util.h @@ -1,143 +1,151 @@ #ifndef __OPKELE_UTIL_H #define __OPKELE_UTIL_H #include <time.h> #include <string> #include <vector> #include <openssl/bn.h> #include <openssl/dh.h> +#include <opkele/types.h> namespace opkele { using std::string; using std::vector; /** * @brief opkele utils namespace */ namespace util { /** * Convenience class encapsulating SSL BIGNUM object for the purpose of * automatical freeing. */ class bignum_t { public: BIGNUM *_bn; bignum_t() : _bn(0) { } bignum_t(BIGNUM *bn) : _bn(bn) { } ~bignum_t() throw() { if(_bn) BN_free(_bn); } bignum_t& operator=(BIGNUM *bn) { if(_bn) BN_free(_bn); _bn = bn; return *this; } operator const BIGNUM*(void) const { return _bn; } operator BIGNUM*(void) { return _bn; } }; /** * Convenience clas encapsulating SSL DH object for the purpose of * automatic freeing. */ class dh_t { public: DH *_dh; dh_t() : _dh(0) { } dh_t(DH *dh) : _dh(dh) { } ~dh_t() throw() { if(_dh) DH_free(_dh); } dh_t& operator=(DH *dh) { if(_dh) DH_free(_dh); _dh = dh; return *this; } operator const DH*(void) const { return _dh; } operator DH*(void) { return _dh; } DH* operator->() { return _dh; } const DH* operator->() const { return _dh; } }; /** * Convert base64-encoded SSL BIGNUM to internal representation. * @param b64 base64-encoded number * @return SSL BIGNUM * @throw failed_conversion in case of error */ BIGNUM *base64_to_bignum(const string& b64); /** * Convert decimal representation to SSL BIGNUM. * @param dec decimal representation * @return resulting BIGNUM * @throw failed_conversion in case of error */ BIGNUM *dec_to_bignum(const string& dec); /** * Convert SSL BIGNUM data to base64 encoded string. * @param bn BIGNUM * @return base64encoded string */ string bignum_to_base64(const BIGNUM *bn); /** * Convert internal time representation to w3c format * @param t internal representation * @return w3c time * @throw failed_conversion in case of error */ string time_to_w3c(time_t t); /** * Convert W3C time representation to internal time_t * @param w w3c representation * @return converted time * @throw failed_conversion in case of error */ time_t w3c_to_time(const string& w); /** * Encode string to the representation suitable for using in URL. * @param str string to encode * @return encoded string * @throw failed_conversion in case of failure */ string url_encode(const string& str); /** * Convert number to string * @param l number * @return string representation * @throw failed_conversion in case of failure */ string long_to_string(long l); /** * Convert string to number * @param s string, containing the number * @return the number * @throw failed_conversion in case of failure */ long string_to_long(const string& s); /** * Encode binary data using base64. * @param data pointer to binary data * @param length length of data * @return encoded data */ string encode_base64(const void *data,size_t length); /** * Decode binary data from base64 representation. * @param data base64-encoded data * @param rv container for decoded binary */ void decode_base64(const string& data,vector<unsigned char>& rv); /** * Normalize http(s) URI according to RFC3986, section 6. URI is * expected to have scheme: in front of it. * @param uri URI * @return normalized URI * @throw not_implemented in case of non-httpi(s) URI * @throw bad_input in case of malformed URI */ string rfc_3986_normalize_uri(const string& uri); + + string& strip_uri_fragment_part(string& uri); + + string abi_demangle(const char* mn); + + string base64_signature(const assoc_t& assoc,const basic_openid_message& om); + } } #endif /* __OPKELE_UTIL_H */ diff --git a/lib/Makefile.am b/lib/Makefile.am index 989de28..c58ec3f 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -1,31 +1,34 @@ lib_LTLIBRARIES = libopkele.la AM_CPPFLAGS = ${CPPFLAGS_DEBUG} DEFAULT_INCLUDES = -I${top_builddir} INCLUDES = \ - -I${top_srcdir}/include/ \ + -I${top_builddir}/include/ -I${top_srcdir}/include/ \ ${KONFORKA_CFLAGS} \ ${OPENSSL_CFLAGS} \ ${LIBCURL_CPPFLAGS} \ ${PCRE_CFLAGS} ${EXPAT_CFLAGS} ${TIDY_CFLAGS} libopkele_la_LIBADD = \ ${LIBCURL} \ ${PCRE_LIBS} ${EXPAT_LIBS} \ ${OPENSSL_LIBS} \ ${KONFORKA_LIBS} ${TIDY_LIBS} libopkele_la_SOURCES = \ params.cc \ util.cc \ server.cc \ secret.cc \ data.cc \ consumer.cc \ exception.cc \ extension.cc \ sreg.cc \ extension_chain.cc \ curl.cc expat.cc \ - discovery.cc + discovery.cc \ + basic_rp.cc \ + prequeue_rp.cc \ + openid_message.cc libopkele_la_LDFLAGS = \ -version-info 2:0:0 diff --git a/lib/basic_rp.cc b/lib/basic_rp.cc new file mode 100644 index 0000000..763a391 --- a/dev/null +++ b/lib/basic_rp.cc @@ -0,0 +1,311 @@ +#include <openssl/sha.h> +#include <openssl/hmac.h> +#include <opkele/basic_rp.h> +#include <opkele/exception.h> +#include <opkele/uris.h> +#include <opkele/data.h> +#include <opkele/util.h> +#include <opkele/curl.h> + +namespace opkele { + + static void dh_get_secret( + secret_t& secret, const basic_openid_message& om, + const char *exp_assoc, const char *exp_sess, + util::dh_t& dh, + size_t d_len, unsigned char *(*d_fun)(const unsigned char*,size_t,unsigned char*) ) try { + if(om.get_field("assoc_type")!=exp_assoc || om.get_field("session_type")!=exp_sess) + throw bad_input(OPKELE_CP_ "Unexpected associate response"); + util::bignum_t s_pub = util::base64_to_bignum(om.get_field("dh_server_public")); + vector<unsigned char> ck(DH_size(dh)+1); + unsigned char *ckptr = &(ck.front())+1; + int cklen = DH_compute_key(ckptr,s_pub,dh); + if(cklen<0) + throw exception_openssl(OPKELE_CP_ "failed to DH_compute_key()"); + if(cklen && (*ckptr)&0x80) { + (*(--ckptr))=0; ++cklen; } + unsigned char key_digest[d_len]; + secret.enxor_from_base64((*d_fun)(ckptr,cklen,key_digest),om.get_field("enc_mac_key")); + }catch(opkele::failed_lookup& ofl) { + throw bad_input(OPKELE_CP_ "Incoherent response from OP"); + } OPKELE_RETHROW + + static void direct_request(basic_openid_message& oum,const basic_openid_message& inm,const string& OP) { + util::curl_pick_t curl = util::curl_pick_t::easy_init(); + if(!curl) + throw exception_curl(OPKELE_CP_ "failed to initialize curl"); + string request = inm.query_string(); + CURLcode r; + (r=curl.misc_sets()) + || (r=curl.easy_setopt(CURLOPT_URL,OP.c_str())) + || (r=curl.easy_setopt(CURLOPT_POST,1)) + || (r=curl.easy_setopt(CURLOPT_POSTFIELDS,request.data())) + || (r=curl.easy_setopt(CURLOPT_POSTFIELDSIZE,request.length())) + || (r=curl.set_write()); + if(r) + throw exception_curl(OPKELE_CP_ "failed to set curly options",r); + if( (r=curl.easy_perform()) ) + throw exception_curl(OPKELE_CP_ "failed to perform curly request",r); + oum.from_keyvalues(curl.response); + } + + + assoc_t basic_RP::associate(const string& OP) { + util::dh_t dh = DH_new(); + if(!dh) + throw exception_openssl(OPKELE_CP_ "failed to DH_new()"); + dh->p = util::dec_to_bignum(data::_default_p); + dh->g = util::dec_to_bignum(data::_default_g); + if(!DH_generate_key(dh)) + throw exception_openssl(OPKELE_CP_ "failed to DH_generate_key()"); + openid_message_t req; + req.set_field("ns",OIURI_OPENID20); + req.set_field("mode","associate"); + req.set_field("dh_modulus",util::bignum_to_base64(dh->p)); + req.set_field("dh_gen",util::bignum_to_base64(dh->g)); + req.set_field("dh_consumer_public",util::bignum_to_base64(dh->pub_key)); + openid_message_t res; + req.set_field("assoc_type","HMAC-SHA256"); + req.set_field("session_type","DH-SHA256"); + secret_t secret; + int expires_in; + try { + direct_request(res,req,OP); + dh_get_secret( secret, res, + "HMAC-SHA256", "DH-SHA256", + dh, SHA256_DIGEST_LENGTH, SHA256 ); + expires_in = util::string_to_long(res.get_field("expires_in")); + }catch(exception& e) { + try { + req.set_field("assoc_type","HMAC-SHA1"); + req.set_field("session_type","DH-SHA1"); + direct_request(res,req,OP); + dh_get_secret( secret, res, + "HMAC-SHA1", "DH-SHA1", + dh, SHA_DIGEST_LENGTH, SHA1 ); + expires_in = util::string_to_long(res.get_field("expires_in")); + }catch(bad_input& e) { + throw dumb_RP(OPKELE_CP_ "OP failed to supply an association"); + } + } + return store_assoc( + OP, res.get_field("assoc_handle"), + res.get_field("assoc_type"), secret, + expires_in ); + } + + basic_openid_message& basic_RP::checkid_( + basic_openid_message& rv, + mode_t mode, + const string& return_to,const string& realm, + extension_t *ext) { + rv.reset_fields(); + rv.set_field("ns",OIURI_OPENID20); + if(mode==mode_checkid_immediate) + rv.set_field("mode","checkid_immediate"); + else if(mode==mode_checkid_setup) + rv.set_field("mode","checkid_setup"); + else + throw bad_input(OPKELE_CP_ "unknown checkid_* mode"); + if(realm.empty() && return_to.empty()) + throw bad_input(OPKELE_CP_ "At least one of realm and return_to must be non-empty"); + if(!realm.empty()) { + rv.set_field("realm",realm); + rv.set_field("trust_root",realm); + } + if(!return_to.empty()) + rv.set_field("return_to",return_to); + const openid_endpoint_t& ep = get_endpoint(); + rv.set_field("claimed_id",ep.claimed_id); + rv.set_field("identity",ep.local_id); + try { + rv.set_field("assoc_handle",find_assoc(ep.uri)->handle()); + }catch(dumb_RP& drp) { + }catch(failed_lookup& fl) { + try { + rv.set_field("assoc_handle",associate(ep.uri)->handle()); + }catch(dumb_RP& drp) { } + } OPKELE_RETHROW + if(ext) ext->checkid_hook(rv); + return rv; + } + + class signed_part_message_proxy : public basic_openid_message { + public: + const basic_openid_message& x; + set<string> signeds; + + signed_part_message_proxy(const basic_openid_message& xx) : x(xx) { + const string& slist = x.get_field("signed"); + string::size_type p = 0; + while(true) { + string::size_type co = slist.find(',',p); + string f = (co==string::npos) + ?slist.substr(p):slist.substr(p,co-p); + signeds.insert(f); + if(co==string::npos) break; + p = co+1; + } + } + + bool has_field(const string& n) const { + return signeds.find(n)!=signeds.end() && x.has_field(n); } + const string& get_field(const string& n) const { + if(signeds.find(n)==signeds.end()) + throw failed_lookup(OPKELE_CP_ "The field isn't known to be signed"); + return x.get_field(n); } + + fields_iterator fields_begin() const { + return signeds.begin(); } + fields_iterator fields_end() const { + return signeds.end(); } + }; + + static void parse_query(const string& u,string::size_type q, + map<string,string>& p) { + if(q==string::npos) + return; + assert(u[q]=='?'); + ++q; + string::size_type l = u.size(); + while(q<l) { + string::size_type eq = u.find('=',q); + string::size_type am = u.find('&',q); + if(am==string::npos) { + if(eq==string::npos) { + p[""] = u.substr(q); + }else{ + p[u.substr(q,eq-q)] = u.substr(eq+1); + } + break; + }else{ + if(eq==string::npos || eq>am) { + p[""] = u.substr(q,eq-q); + }else{ + p[u.substr(q,eq-q)] = u.substr(eq+1,am-eq-1); + } + q = ++am; + } + } + } + + void basic_RP::id_res(const basic_openid_message& om,extension_t *ext) { + bool o2 = om.has_field("ns") + && om.get_field("ns")==OIURI_OPENID20; + if( (!o2) && om.has_field("user_setup_url")) + throw id_res_setup(OPKELE_CP_ "assertion failed, setup url provided", + om.get_field("user_setup_url")); + string m = om.get_field("mode"); + if(o2 && m=="setup_needed") + throw id_res_setup(OPKELE_CP_ "setup needed, no setup url provided"); + if(m=="cancel") + throw id_res_cancel(OPKELE_CP_ "authentication cancelled"); + bool go_dumb=false; + try { + string OP = o2 + ?om.get_field("op_endpoint") + :get_endpoint().uri; + assoc_t assoc = retrieve_assoc( + OP,om.get_field("assoc_handle")); + if(om.get_field("sig")!=util::base64_signature(assoc,om)) + throw id_res_mismatch(OPKELE_CP_ "signature mismatch"); + }catch(dumb_RP& drp) { + go_dumb=true; + }catch(failed_lookup& e) { + go_dumb=true; + } OPKELE_RETHROW + if(go_dumb) { + try { + string OP = o2 + ?om.get_field("op_endpoint") + :get_endpoint().uri; + check_authentication(OP,om); + }catch(failed_check_authentication& fca) { + throw id_res_failed(OPKELE_CP_ "failed to check_authentication()"); + } OPKELE_RETHROW + } + signed_part_message_proxy signeds(om); + if(o2) { + check_nonce(om.get_field("op_endpoint"), + om.get_field("response_nonce")); + static const char *mustsign[] = { + "op_endpoint", "return_to", "response_nonce", "assoc_handle", + "claimed_id", "identity" }; + for(int ms=0;ms<(sizeof(mustsign)/sizeof(*mustsign));++ms) { + if(om.has_field(mustsign[ms]) && !signeds.has_field(mustsign[ms])) + throw bad_input(OPKELE_CP_ string("Field '")+mustsign[ms]+"' is not signed against the specs"); + } + if( ( + (om.has_field("claimed_id")?1:0) + ^ + (om.has_field("identity")?1:0) + )&1 ) + throw bad_input(OPKELE_CP_ "claimed_id and identity must be either both present or both absent"); + + string turl = util::rfc_3986_normalize_uri(get_this_url()); + util::strip_uri_fragment_part(turl); + string rurl = util::rfc_3986_normalize_uri(om.get_field("return_to")); + util::strip_uri_fragment_part(rurl); + string::size_type + tq = turl.find('?'), rq = rurl.find('?'); + if( + ((tq==string::npos)?turl:turl.substr(0,tq)) + != + ((rq==string::npos)?rurl:rurl.substr(0,rq)) + ) + throw id_res_bad_return_to(OPKELE_CP_ "return_to url doesn't match request url"); + map<string,string> tp; parse_query(turl,tq,tp); + map<string,string> rp; parse_query(rurl,rq,rp); + for(map<string,string>::const_iterator rpi=rp.begin();rpi!=rp.end();++rpi) { + map<string,string>::const_iterator tpi = tp.find(rpi->first); + if(tpi==tp.end()) + throw id_res_bad_return_to(OPKELE_CP_ string("Parameter '")+rpi->first+"' from return_to is missing from the request"); + if(tpi->second!=rpi->second) + throw id_res_bad_return_to(OPKELE_CP_ string("Parameter '")+rpi->first+"' from return_to doesn't matche the request"); + } + + if(om.has_field("claimed_id")) { + verify_OP( + om.get_field("op_endpoint"), + om.get_field("claimed_id"), + om.get_field("identity") ); + } + + } + if(ext) ext->id_res_hook(om,signeds); + } + + class check_auth_message_proxy : public basic_openid_message { + public: + const basic_openid_message& x; + + check_auth_message_proxy(const basic_openid_message& xx) : x(xx) { } + + bool has_field(const string& n) const { return x.has_field(n); } + const string& get_field(const string& n) const { + static const string checkauthmode="check_authentication"; + return (n=="mode")?checkauthmode:x.get_field(n); } + bool has_ns(const string& uri) const {return x.has_ns(uri); } + string get_ns(const string& uri) const { return x.get_ns(uri); } + fields_iterator fields_begin() const { + return x.fields_begin(); } + fields_iterator fields_end() const { + return x.fields_end(); } + }; + + void basic_RP::check_authentication(const string& OP, + const basic_openid_message& om){ + openid_message_t res; + direct_request(res,check_auth_message_proxy(om),OP); + if(res.has_field("is_valid")) { + if(res.get_field("is_valid")=="true") { + if(res.has_field("invalidate_handle")) + invalidate_assoc(OP,res.get_field("invalidate_handle")); + return; + } + } + throw failed_check_authentication( + OPKELE_CP_ "failed to verify response"); + } + +} diff --git a/lib/consumer.cc b/lib/consumer.cc index 3c3b4f8..ebda262 100644 --- a/lib/consumer.cc +++ b/lib/consumer.cc @@ -1,395 +1,395 @@ #include <algorithm> #include <cassert> #include <cstring> #include <opkele/util.h> #include <opkele/curl.h> #include <opkele/exception.h> #include <opkele/data.h> #include <opkele/consumer.h> #include <openssl/sha.h> #include <openssl/hmac.h> #include <iostream> #include "config.h" #include <pcre.h> namespace opkele { using namespace std; using util::curl_t; using util::curl_pick_t; class pcre_matches_t { public: int *_ov; int _s; pcre_matches_t() : _ov(0), _s(0) { } pcre_matches_t(int s) : _ov(0), _s(s) { if(_s&1) ++_s; _s += _s>>1; _ov = new int[_s]; } ~pcre_matches_t() throw() { if(_ov) delete[] _ov; } int begin(int i) const { return _ov[i<<1]; } int end(int i) const { return _ov[(i<<1)+1]; } int length(int i) const { int t=i<<1; return _ov[t+1]-_ov[t]; } }; class pcre_t { public: pcre *_p; pcre_t() : _p(0) { } pcre_t(pcre *p) : _p(p) { } pcre_t(const char *re,int opts) : _p(0) { static const char *errptr; static int erroffset; _p = pcre_compile(re,opts,&errptr,&erroffset,NULL); if(!_p) throw internal_error(OPKELE_CP_ string("Failed to compile regexp: ")+errptr); } ~pcre_t() throw() { if(_p) (*pcre_free)(_p); } pcre_t& operator=(pcre *p) { if(_p) (*pcre_free)(_p); _p=p; return *this; } operator const pcre*(void) const { return _p; } operator pcre*(void) { return _p; } int exec(const string& s,pcre_matches_t& m) { if(!_p) throw internal_error(OPKELE_CP_ "Trying to execute absent regexp"); return pcre_exec(_p,NULL,s.c_str(),s.length(),0,0,m._ov,m._s); } }; assoc_t consumer_t::associate(const string& server) { util::dh_t dh = DH_new(); if(!dh) throw exception_openssl(OPKELE_CP_ "failed to DH_new()"); dh->p = util::dec_to_bignum(data::_default_p); dh->g = util::dec_to_bignum(data::_default_g); if(!DH_generate_key(dh)) throw exception_openssl(OPKELE_CP_ "failed to DH_generate_key()"); string request = "openid.mode=associate" "&openid.assoc_type=HMAC-SHA1" "&openid.session_type=DH-SHA1" "&openid.dh_consumer_public="; request += util::url_encode(util::bignum_to_base64(dh->pub_key)); curl_pick_t curl = curl_pick_t::easy_init(); if(!curl) throw exception_curl(OPKELE_CP_ "failed to initialize curl"); CURLcode r; (r=curl.misc_sets()) || (r=curl.easy_setopt(CURLOPT_URL,server.c_str())) || (r=curl.easy_setopt(CURLOPT_POST,1)) || (r=curl.easy_setopt(CURLOPT_POSTFIELDS,request.data())) || (r=curl.easy_setopt(CURLOPT_POSTFIELDSIZE,request.length())) || (r=curl.set_write()) ; if(r) throw exception_curl(OPKELE_CP_ "failed to set curly options",r); if( (r=curl.easy_perform()) ) throw exception_curl(OPKELE_CP_ "failed to perform curly request",r); params_t p; p.parse_keyvalues(curl.response); if(p.has_param("assoc_type") && p.get_param("assoc_type")!="HMAC-SHA1") throw bad_input(OPKELE_CP_ "unsupported assoc_type"); string st; if(p.has_param("session_type")) st = p.get_param("session_type"); if((!st.empty()) && st!="DH-SHA1") throw bad_input(OPKELE_CP_ "unsupported session_type"); secret_t secret; if(st.empty()) { secret.from_base64(p.get_param("mac_key")); }else{ util::bignum_t s_pub = util::base64_to_bignum(p.get_param("dh_server_public")); vector<unsigned char> ck(DH_size(dh)+1); unsigned char *ckptr = &(ck.front())+1; int cklen = DH_compute_key(ckptr,s_pub,dh); if(cklen<0) throw exception_openssl(OPKELE_CP_ "failed to DH_compute_key()"); if(cklen && (*ckptr)&0x80) { (*(--ckptr)) = 0; ++cklen; } unsigned char key_sha1[SHA_DIGEST_LENGTH]; SHA1(ckptr,cklen,key_sha1); secret.enxor_from_base64(key_sha1,p.get_param("enc_mac_key")); } int expires_in = 0; if(p.has_param("expires_in")) { expires_in = util::string_to_long(p.get_param("expires_in")); }else if(p.has_param("issued") && p.has_param("expiry")) { expires_in = util::w3c_to_time(p.get_param("expiry"))-util::w3c_to_time(p.get_param("issued")); }else throw bad_input(OPKELE_CP_ "no expiration information"); return store_assoc(server,p.get_param("assoc_handle"),secret,expires_in); } string consumer_t::checkid_immediate(const string& identity,const string& return_to,const string& trust_root,extension_t *ext) { return checkid_(mode_checkid_immediate,identity,return_to,trust_root,ext); } string consumer_t::checkid_setup(const string& identity,const string& return_to,const string& trust_root,extension_t *ext) { return checkid_(mode_checkid_setup,identity,return_to,trust_root,ext); } string consumer_t::checkid_(mode_t mode,const string& identity,const string& return_to,const string& trust_root,extension_t *ext) { params_t p; if(mode==mode_checkid_immediate) p["mode"]="checkid_immediate"; else if(mode==mode_checkid_setup) p["mode"]="checkid_setup"; else throw bad_input(OPKELE_CP_ "unknown checkid_* mode"); string iurl = canonicalize(identity); string server, delegate; retrieve_links(iurl,server,delegate); p["identity"] = delegate.empty()?iurl:delegate; if(!trust_root.empty()) p["trust_root"] = trust_root; p["return_to"] = return_to; try { string ah = find_assoc(server)->handle(); p["assoc_handle"] = ah; }catch(failed_lookup& fl) { string ah = associate(server)->handle(); p["assoc_handle"] = ah; } - if(ext) ext->checkid_hook(p,identity); + if(ext) ext->checkid_hook(p); return p.append_query(server); } void consumer_t::id_res(const params_t& pin,const string& identity,extension_t *ext) { if(pin.has_param("openid.user_setup_url")) throw id_res_setup(OPKELE_CP_ "assertion failed, setup url provided",pin.get_param("openid.user_setup_url")); string server,delegate; retrieve_links(identity.empty()?pin.get_param("openid.identity"):canonicalize(identity),server,delegate); params_t ps; try { assoc_t assoc = retrieve_assoc(server,pin.get_param("openid.assoc_handle")); if(assoc->is_expired()) throw id_res_expired_on_delivery(OPKELE_CP_ "retrieve_assoc() has returned expired handle"); const string& sigenc = pin.get_param("openid.sig"); vector<unsigned char> sig; util::decode_base64(sigenc,sig); const string& slist = pin.get_param("openid.signed"); string kv; string::size_type p = 0; while(true) { string::size_type co = slist.find(',',p); string f = (co==string::npos)?slist.substr(p):slist.substr(p,co-p); kv += f; kv += ':'; f.insert(0,"openid."); kv += pin.get_param(f); kv += '\n'; if(ext) ps[f.substr(sizeof("openid.")-1)] = pin.get_param(f); if(co==string::npos) break; p = co+1; } secret_t secret = assoc->secret(); unsigned int md_len = 0; unsigned char *md = HMAC( EVP_sha1(), &(secret.front()),secret.size(), (const unsigned char *)kv.data(),kv.length(), 0,&md_len); if(sig.size()!=md_len || memcmp(&(sig.front()),md,md_len)) throw id_res_mismatch(OPKELE_CP_ "signature mismatch"); }catch(failed_lookup& e) { const string& slist = pin.get_param("openid.signed"); string::size_type pp = 0; params_t p; while(true) { string::size_type co = slist.find(',',pp); string f = "openid."; f += (co==string::npos)?slist.substr(pp):slist.substr(pp,co-pp); p[f] = pin.get_param(f); if(co==string::npos) break; pp = co+1; } p["openid.assoc_handle"] = pin.get_param("openid.assoc_handle"); p["openid.sig"] = pin.get_param("openid.sig"); p["openid.signed"] = pin.get_param("openid.signed"); try { string ih = pin.get_param("openid.invalidate_handle"); p["openid.invalidate_handle"] = ih; }catch(failed_lookup& fl) { } try { check_authentication(server,p); }catch(failed_check_authentication& fca) { throw id_res_failed(OPKELE_CP_ "failed to check_authentication()"); } } - if(ext) ext->id_res_hook(pin,ps,identity); + if(ext) ext->id_res_hook(pin,ps); } void consumer_t::check_authentication(const string& server,const params_t& p) { string request = "openid.mode=check_authentication"; for(params_t::const_iterator i=p.begin();i!=p.end();++i) { if(i->first!="openid.mode") { request += '&'; request += i->first; request += '='; request += util::url_encode(i->second); } } curl_pick_t curl = curl_pick_t::easy_init(); if(!curl) throw exception_curl(OPKELE_CP_ "failed to initialize curl"); CURLcode r; (r=curl.misc_sets()) || (r=curl.easy_setopt(CURLOPT_URL,server.c_str())) || (r=curl.easy_setopt(CURLOPT_POST,1)) || (r=curl.easy_setopt(CURLOPT_POSTFIELDS,request.data())) || (r=curl.easy_setopt(CURLOPT_POSTFIELDSIZE,request.length())) || (r=curl.set_write()) ; if(r) throw exception_curl(OPKELE_CP_ "failed to set curly options",r); if( (r=curl.easy_perform()) ) throw exception_curl(OPKELE_CP_ "failed to perform curly request",r); params_t pp; pp.parse_keyvalues(curl.response); if(pp.has_param("invalidate_handle")) invalidate_assoc(server,pp.get_param("invalidate_handle")); if(pp.has_param("is_valid")) { if(pp.get_param("is_valid")=="true") return; }else if(pp.has_param("lifetime")) { if(util::string_to_long(pp.get_param("lifetime"))) return; } throw failed_check_authentication(OPKELE_CP_ "failed to verify response"); } void consumer_t::retrieve_links(const string& url,string& server,string& delegate) { server.erase(); delegate.erase(); curl_pick_t curl = curl_pick_t::easy_init(); if(!curl) throw exception_curl(OPKELE_CP_ "failed to initialize curl"); string& html = curl.response; CURLcode r; (r=curl.misc_sets()) || (r=curl.easy_setopt(CURLOPT_URL,url.c_str())) || (r=curl.set_write()); ; if(r) throw exception_curl(OPKELE_CP_ "failed to set curly options",r); r = curl.easy_perform(); if(r && r!=CURLE_WRITE_ERROR) throw exception_curl(OPKELE_CP_ "failed to perform curly request",r); static const char *re_bre = "<\\s*body\\b", *re_hdre = "<\\s*head[^>]*>", *re_lre = "<\\s*link\\b([^>]+)>", *re_rre = "\\brel\\s*=\\s*['\"]([^'\"]+)['\"]", *re_hre = "\\bhref\\s*=\\s*['\"]\\s*([^'\"\\s]+)\\s*['\"]"; pcre_matches_t m1(3), m2(3); pcre_t bre(re_bre,PCRE_CASELESS); if(bre.exec(html,m1)>0) html.erase(m1.begin(0)); pcre_t hdre(re_hdre,PCRE_CASELESS); if(hdre.exec(html,m1)<=0) throw bad_input(OPKELE_CP_ "failed to find <head>"); html.erase(0,m1.end(0)+1); pcre_t lre(re_lre,PCRE_CASELESS), rre(re_rre,PCRE_CASELESS), hre(re_hre,PCRE_CASELESS); bool gotit = false; while( (!gotit) && lre.exec(html,m1)>=2 ) { static const char *whitespace = " \t"; string attrs(html,m1.begin(1),m1.length(1)); html.erase(0,m1.end(0)+1); if(!( rre.exec(attrs,m1)>=2 && hre.exec(attrs,m2)>=2 )) continue; string rels(attrs,m1.begin(1),m1.length(1)); for(string::size_type ns = rels.find_first_not_of(whitespace); ns!=string::npos; ns=rels.find_first_not_of(whitespace,ns)) { string::size_type s = rels.find_first_of(whitespace,ns); string rel; if(s==string::npos) { rel.assign(rels,ns,string::npos); ns=string::npos; }else{ rel.assign(rels,ns,s-ns); ns=s; } if(rel=="openid.server") { server.assign(attrs,m2.begin(1),m2.length(1)); if(!delegate.empty()) { gotit = true; break; } }else if(rel=="openid.delegate") { delegate.assign(attrs,m2.begin(1),m2.length(1)); if(!server.empty()) { gotit = true; break; } } } } if(server.empty()) throw failed_assertion(OPKELE_CP_ "The location has no openid.server declaration"); } assoc_t consumer_t::find_assoc(const string& /* server */) { throw failed_lookup(OPKELE_CP_ "no find_assoc() provided"); } string consumer_t::normalize(const string& url) { string rv = url; // strip leading and trailing spaces string::size_type i = rv.find_first_not_of(" \t\r\n"); if(i==string::npos) throw bad_input(OPKELE_CP_ "empty URL"); if(i) rv.erase(0,i); i = rv.find_last_not_of(" \t\r\n"); assert(i!=string::npos); if(i<(rv.length()-1)) rv.erase(i+1); // add missing http:// i = rv.find("://"); if(i==string::npos) { // primitive. but do we need more? rv.insert(0,"http://"); i = sizeof("http://")-1; }else{ i += sizeof("://")-1; } string::size_type qm = rv.find('?',i); string::size_type sl = rv.find('/',i); if(qm!=string::npos) { if(sl==string::npos || sl>qm) rv.insert(qm,1,'/'); }else{ if(sl==string::npos) rv += '/'; } return rv; } string consumer_t::canonicalize(const string& url) { string rv = normalize(url); curl_t curl = curl_t::easy_init(); if(!curl) throw exception_curl(OPKELE_CP_ "failed to initialize curl()"); string html; CURLcode r; (r=curl.misc_sets()) || (r=curl.easy_setopt(CURLOPT_URL,rv.c_str())) || (r=curl.easy_setopt(CURLOPT_NOBODY,1)) ; if(r) throw exception_curl(OPKELE_CP_ "failed to set curly options",r); r = curl.easy_perform(); if(r) throw exception_curl(OPKELE_CP_ "failed to perform curly request",r); const char *eu = 0; r = curl.easy_getinfo(CURLINFO_EFFECTIVE_URL,&eu); if(r) throw exception_curl(OPKELE_CP_ "failed to get CURLINFO_EFFECTIVE_URL",r); rv = eu; return normalize(rv); } } diff --git a/lib/discovery.cc b/lib/discovery.cc index d868308..93409f4 100644 --- a/lib/discovery.cc +++ b/lib/discovery.cc @@ -1,446 +1,537 @@ #include <list> #include <opkele/curl.h> #include <opkele/expat.h> #include <opkele/uris.h> #include <opkele/discovery.h> #include <opkele/exception.h> #include <opkele/util.h> #include <opkele/tidy.h> #include <opkele/debug.h> #include "config.h" #define XRDS_HEADER "X-XRDS-Location" #define CT_HEADER "Content-Type" namespace opkele { using std::list; using xrd::XRD_t; using xrd::service_t; + /* TODO: the whole discovery thing needs cleanup and optimization due to + * many changes of concept. */ + static const char *whitespace = " \t\r\n"; static const char *i_leaders = "=@+$!("; static const size_t max_html = 16384; + static const struct service_type_t { + const char *uri; + const char *forceid; + } service_types[] = { + { STURI_OPENID20_OP, IDURI_SELECT20 }, + { STURI_OPENID20, 0 }, + { STURI_OPENID11, 0 }, + { STURI_OPENID10, 0 } + }; + enum { + st_index_1 = 2, st_index_2 = 1 + }; + + static inline bool is_qelement(const XML_Char *n,const char *qen) { return !strcasecmp(n,qen); } static inline bool is_element(const XML_Char *n,const char *en) { if(!strcasecmp(n,en)) return true; int nl = strlen(n), enl = strlen(en); if( (nl>=(enl+1)) && n[nl-enl-1]=='\t' && !strcasecmp(&n[nl-enl],en) ) return true; return false; } static long element_priority(const XML_Char **a) { for(;*a;++a) if(!strcasecmp(*(a++),"priority")) { long rv; return (sscanf(*a,"%ld",&rv)==1)?rv:-1; } return -1; } class idigger_t : public util::curl_t, public util::expat_t { public: string xri_proxy; enum { - xmode_html = 1, xmode_xrd = 2 + xmode_html = 1, xmode_xrd = 2, xmode_cid = 4 }; int xmode; string xrds_location; string http_content_type; service_t html_openid1; service_t html_openid2; string cdata_buf; long status_code; string status_string; typedef list<string> pt_stack_t; pt_stack_t pt_stack; int skipping; bool parser_choked; string save_html; XRD_t *xrd; service_t *xrd_service; string* cdata; idigger_t() : util::curl_t(easy_init()), util::expat_t(0), xri_proxy(XRI_PROXY_URL) { CURLcode r; (r=misc_sets()) || (r=set_write()) || (r=set_header()) ; if(r) throw exception_curl(OPKELE_CP_ "failed to set curly options",r); } ~idigger_t() throw() { } - void discover(idiscovery_t& result,const string& identity) { - result.clear(); + string discover(endpoint_discovery_iterator& oi,const string& identity) { + string rv; + idiscovery_t idis; string::size_type fsc = identity.find_first_not_of(whitespace); if(fsc==string::npos) - throw bad_input(OPKELE_CP_ "whtiespace-only identity"); + throw bad_input(OPKELE_CP_ "whitespace-only identity"); string::size_type lsc = identity.find_last_not_of(whitespace); assert(lsc!=string::npos); if(!strncasecmp(identity.c_str()+fsc,"xri://",sizeof("xri://")-1)) fsc += sizeof("xri://")-1; if((fsc+1)>=lsc) throw bad_input(OPKELE_CP_ "not a character of importance in identity"); string id(identity,fsc,lsc-fsc+1); + idis.clear(); if(strchr(i_leaders,id[0])) { - result.normalized_id = id; - result.xri_identity = true; - /* TODO: further canonicalize xri identity? Like folding case or whatever... */ - discover_at( - result, - xri_proxy + util::url_encode(id)+ - "?_xrd_r=application/xrd+xml;sep=false", xmode_xrd); - if(status_code!=100) - throw failed_xri_resolution(OPKELE_CP_ - "XRI resolution failed with '"+status_string+"' message",status_code); - if(result.xrd.canonical_ids.empty()) - throw opkele::failed_discovery(OPKELE_CP_ "No CanonicalID for XRI identity found"); - result.canonicalized_id = result.xrd.canonical_ids.begin()->second; + /* TODO: further normalize xri identity? Like folding case + * or whatever... */ + rv = idis.normalized_id = id; + idis.xri_identity = true; + set<string> cids; + for(const struct service_type_t *st=service_types; + st<&service_types[sizeof(service_types)/sizeof(*service_types)];++st) { + idis.clear(); + discover_at( idis, + xri_proxy + util::url_encode(id)+ + "?_xrd_t="+util::url_encode(st->uri)+ + "&_xrd_r=application/xrd%2Bxml" + ";sep=true;refs=true", + xmode_xrd ); + if(status_code==241) continue; + if(status_code!=100) + throw failed_xri_resolution(OPKELE_CP_ + "XRI resolution failed with '"+status_string+"' message" + ", while looking for SEP with type '"+st->uri+"'", status_code); + if(idis.xrd.canonical_ids.empty()) + throw opkele::failed_discovery(OPKELE_CP_ "No CanonicalID found for XRI identity found"); + string cid = idis.xrd.canonical_ids.begin()->second; + if(cids.find(cid)==cids.end()) { + cids.insert(cid); + idis.clear(); + discover_at( idis, + xri_proxy + util::url_encode(id)+ + "?_xrd_t="+util::url_encode(st->uri)+ + "&_xrd_r=application/xrd%2Bxml" + ";sep=true;refs=true", + xmode_xrd ); + if(status_code==241) continue; + if(status_code!=100) + throw failed_xri_resolution(OPKELE_CP_ + "XRI resolution failed with '"+status_string+"' message" + ", while looking for SEP with type '"+st->uri+"'" + " on canonical id", status_code); + } + idis.canonicalized_id = cid; + queue_endpoints(oi,idis,st); + } }else{ - result.xri_identity = false; + idis.xri_identity = false; if(id.find("://")==string::npos) id.insert(0,"http://"); string::size_type fp = id.find('#'); if(fp!=string::npos) { string::size_type qp = id.find('?'); if(qp==string::npos || qp<fp) id.erase(fp); else if(qp>fp) id.erase(fp,qp-fp); } - result.normalized_id = util::rfc_3986_normalize_uri(id); - discover_at(result,id,xmode_html|xmode_xrd); + rv = idis.normalized_id = util::rfc_3986_normalize_uri(id); + discover_at(idis,id,xmode_html|xmode_xrd); const char * eu = 0; CURLcode r = easy_getinfo(CURLINFO_EFFECTIVE_URL,&eu); if(r) throw exception_curl(OPKELE_CP_ "failed to get CURLINFO_EFFECTIVE_URL",r); - result.canonicalized_id = util::rfc_3986_normalize_uri(eu); /* XXX: strip fragment part? */ + string cid = util::strip_uri_fragment_part( idis.canonicalized_id = util::rfc_3986_normalize_uri(eu) ); if(xrds_location.empty()) { - html2xrd(result.xrd); + html2xrd(oi,idis); }else{ - discover_at(result,xrds_location,xmode_xrd); - if(result.xrd.empty()) - html2xrd(result.xrd); + idis.clear(); + idis.canonicalized_id = cid; + discover_at(idis,xrds_location,xmode_xrd); + if(idis.xrd.empty()) + html2xrd(oi,idis); + else{ + for(const service_type_t *st=service_types; + st<&service_types[sizeof(service_types)/sizeof(*service_types)];++st) + queue_endpoints(oi,idis,st); + } } } + return rv; } - void discover_at(idiscovery_t& result,const string& url,int xm) { + void discover_at(idiscovery_t& idis,const string& url,int xm) { + DOUT_("Doing discovery at " << url); CURLcode r = easy_setopt(CURLOPT_URL,url.c_str()); if(r) throw exception_curl(OPKELE_CP_ "failed to set culry urlie",r); http_content_type.clear(); xmode = xm; prepare_to_parse(); if(xmode&xmode_html) { xrds_location.clear(); save_html.clear(); save_html.reserve(max_html); } - xrd = &result.xrd; + xrd = &idis.xrd; r = easy_perform(); if(r && r!=CURLE_WRITE_ERROR) throw exception_curl(OPKELE_CP_ "failed to perform curly request",r); if(!parser_choked) { parse(0,0,true); }else{ /* TODO: do not bother if we've seen xml */ try { util::tidy_doc_t td = util::tidy_doc_t::create(); if(!td) throw exception_tidy(OPKELE_CP_ "failed to create htmltidy document"); #ifndef NDEBUG td.opt_set(TidyQuiet,false); td.opt_set(TidyShowWarnings,false); #endif /* NDEBUG */ td.opt_set(TidyForceOutput,true); td.opt_set(TidyXhtmlOut,true); td.opt_set(TidyDoctypeMode,TidyDoctypeOmit); td.opt_set(TidyMark,false); if(td.parse_string(save_html)<=0) throw exception_tidy(OPKELE_CP_ "tidy failed to parse document"); if(td.clean_and_repair()<=0) throw exception_tidy(OPKELE_CP_ "tidy failed to clean and repair"); util::tidy_buf_t tide; if(td.save_buffer(tide)<=0) throw exception_tidy(OPKELE_CP_ "tidy failed to save buffer"); prepare_to_parse(); parse(tide.c_str(),tide.size(),true); }catch(exception_tidy& et) { } } save_html.clear(); } void prepare_to_parse() { (*(expat_t*)this) = parser_create_ns(); set_user_data(); set_element_handler(); set_character_data_handler(); if(xmode&xmode_html) { html_openid1.clear(); html_openid2.clear(); parser_choked = false; } cdata = 0; xrd_service = 0; skipping = 0; + pt_stack.clear(); status_code = 100; status_string.clear(); } - void html2xrd(XRD_t& x) { - if(!html_openid1.uris.empty()) { - html_openid1.types.insert(STURI_OPENID11); - x.services.add(-1,html_openid1); - } + void html2xrd(endpoint_discovery_iterator& oi,idiscovery_t& id) { + XRD_t& x = id.xrd; if(!html_openid2.uris.empty()) { html_openid2.types.insert(STURI_OPENID20); x.services.add(-1,html_openid2); + queue_endpoints(oi,id,&service_types[st_index_2]); + } + if(!html_openid1.uris.empty()) { + html_openid1.types.insert(STURI_OPENID11); + x.services.add(-1,html_openid1); + queue_endpoints(oi,id,&service_types[st_index_1]); } } size_t write(void *p,size_t s,size_t nm) { /* TODO: limit total size */ size_t bytes = s*nm; const char *inbuf = (const char*)p; if(xmode&xmode_html) { size_t mbts = save_html.capacity()-save_html.size(); size_t bts = 0; if(mbts>0) { bts = (bytes>mbts)?mbts:bytes; save_html.append(inbuf,bts); } if(skipping<0) return bts; } if(skipping<0) return 0; bool rp = parse(inbuf,bytes,false); if(!rp) { parser_choked = true; skipping = -1; if(!(xmode&xmode_html)) bytes = 0; } return bytes; } size_t header(void *p,size_t s,size_t nm) { size_t bytes = s*nm; const char *h = (const char*)p; const char *colon = (const char*)memchr(p,':',bytes); const char *space = (const char*)memchr(p,' ',bytes); if(space && ( (!colon) || space<colon ) ) { xrds_location.clear(); http_content_type.clear(); }else if(colon) { const char *hv = ++colon; int hnl = colon-h; int rb; for(rb = bytes-hnl-1;rb>0 && isspace(*hv);++hv,--rb); while(rb>0 && isspace(hv[rb-1])) --rb; if(rb) { if( (hnl>=sizeof(XRDS_HEADER)) && !strncasecmp(h,XRDS_HEADER":", sizeof(XRDS_HEADER)) ) { xrds_location.assign(hv,rb); }else if( (hnl>=sizeof(CT_HEADER)) && !strncasecmp(h,CT_HEADER":", sizeof(CT_HEADER)) ) { const char *sc = (const char*)memchr( hv,';',rb); http_content_type.assign(hv,sc?(sc-hv):rb); } } } return curl_t::header(p,s,nm); } void start_element(const XML_Char *n,const XML_Char **a) { if(skipping<0) return; if(skipping) { if(xmode&xmode_html) html_start_element(n,a); ++skipping; return; } if(pt_stack.empty()) { if(is_qelement(n,NSURI_XRDS "\tXRDS")) return; if(is_qelement(n,NSURI_XRD "\tXRD")) { assert(xrd); xrd->clear(); pt_stack.push_back(n); }else if(xmode&xmode_html) { html_start_element(n,a); }else{ skipping = -1; } }else{ int pt_s = pt_stack.size(); if(pt_s==1) { if(is_qelement(n,NSURI_XRD "\tCanonicalID")) { assert(xrd); cdata = &(xrd->canonical_ids.add(element_priority(a),string())); }else if(is_qelement(n,NSURI_XRD "\tLocalID")) { assert(xrd); cdata = &(xrd->local_ids.add(element_priority(a),string())); }else if(is_qelement(n,NSURI_XRD "\tProviderID")) { assert(xrd); cdata = &(xrd->provider_id); }else if(is_qelement(n,NSURI_XRD "\tService")) { assert(xrd); xrd_service = &(xrd->services.add(element_priority(a), service_t())); pt_stack.push_back(n); }else if(is_qelement(n,NSURI_XRD "\tStatus")) { for(;*a;) { if(!strcasecmp(*(a++),"code")) { if(sscanf(*(a++),"%ld",&status_code)==1 && status_code!=100) { cdata = &status_string; pt_stack.push_back(n); break; } - } + }else + ++a; } }else if(is_qelement(n,NSURI_XRD "\tExpires")) { assert(xrd); cdata_buf.clear(); cdata = &cdata_buf; }else if(xmode&xmode_html) { html_start_element(n,a); }else{ skipping = 1; } }else if(pt_s==2) { if(is_qelement(pt_stack.back().c_str(), NSURI_XRD "\tService")) { if(is_qelement(n,NSURI_XRD "\tType")) { assert(xrd); assert(xrd_service); cdata_buf.clear(); cdata = &cdata_buf; }else if(is_qelement(n,NSURI_XRD "\tURI")) { assert(xrd); assert(xrd_service); cdata = &(xrd_service->uris.add(element_priority(a),string())); }else if(is_qelement(n,NSURI_XRD "\tLocalID") || is_qelement(n,NSURI_OPENID10 "\tDelegate") ) { assert(xrd); assert(xrd_service); cdata = &(xrd_service->local_ids.add(element_priority(a),string())); }else if(is_qelement(n,NSURI_XRD "\tProviderID")) { assert(xrd); assert(xrd_service); cdata = &(xrd_service->provider_id); }else{ skipping = 1; } }else skipping = 1; }else if(xmode&xmode_html) { html_start_element(n,a); }else{ skipping = 1; } } } void end_element(const XML_Char *n) { if(skipping<0) return; if(skipping) { --skipping; return; } if(is_qelement(n,NSURI_XRD "\tType")) { assert(xrd); assert(xrd_service); assert(cdata==&cdata_buf); xrd_service->types.insert(cdata_buf); }else if(is_qelement(n,NSURI_XRD "\tService")) { assert(xrd); assert(xrd_service); assert(!pt_stack.empty()); assert(pt_stack.back()==(NSURI_XRD "\tService")); pt_stack.pop_back(); xrd_service = 0; }else if(is_qelement(n,NSURI_XRD "\tStatus")) { assert(xrd); if(is_qelement(pt_stack.back().c_str(),n)) { assert(cdata==&status_string); pt_stack.pop_back(); if(status_code!=100) skipping = -1; } }else if(is_qelement(n,NSURI_XRD "\tExpires")) { assert(xrd); xrd->expires = util::w3c_to_time(cdata_buf); }else if((xmode&xmode_html) && is_element(n,"head")) { skipping = -1; } cdata = 0; } void character_data(const XML_Char *s,int l) { if(skipping) return; if(cdata) cdata->append(s,l); } void html_start_element(const XML_Char *n,const XML_Char **a) { if(is_element(n,"meta")) { bool heq = false; string l; for(;*a;a+=2) { if(!( strcasecmp(a[0],"http-equiv") || strcasecmp(a[1],XRDS_HEADER) )) heq = true; else if(!strcasecmp(a[0],"content")) l.assign(a[1]); } if(heq) xrds_location = l; }else if(is_element(n,"link")) { string rels; string href; for(;*a;a+=2) { if( !strcasecmp(a[0],"rel") ) { rels.assign(a[1]); }else if( !strcasecmp(a[0],"href") ) { const char *ns = a[1]; for(;*ns && isspace(*ns);++ns); href.assign(ns); string::size_type lns=href.find_last_not_of(whitespace); href.erase(lns+1); } } for(string::size_type ns=rels.find_first_not_of(whitespace); ns!=string::npos; ns=rels.find_first_not_of(whitespace,ns)) { string::size_type s = rels.find_first_of(whitespace,ns); string rel; if(s==string::npos) { rel.assign(rels,ns,string::npos); ns = string::npos; }else{ rel.assign(rels,ns,s-ns); ns = s; } if(rel=="openid.server") html_openid1.uris.add(-1,href); else if(rel=="openid.delegate") html_openid1.local_ids.add(-1,href); else if(rel=="openid2.provider") html_openid2.uris.add(-1,href); else if(rel=="openid2.local_id") html_openid2.local_ids.add(-1,href); } }else if(is_element(n,"body")) { skipping = -1; } } + void queue_endpoints(endpoint_discovery_iterator& oi, + const idiscovery_t &id, + const service_type_t *st) { + openid_endpoint_t ep; + ep.claimed_id = id.canonicalized_id; + for(xrd::services_t::const_iterator isvc=id.xrd.services.begin(); + isvc!=id.xrd.services.end(); ++isvc) { + const xrd::service_t svc = isvc->second; + if(svc.types.find(st->uri)==svc.types.end()) continue; + for(xrd::uris_t::const_iterator iu=svc.uris.begin();iu!=svc.uris.end();++iu) { + ep.uri = iu->second; + if(st->forceid) { + ep.local_id = ep.claimed_id = st->forceid; + *(oi++) = ep; + }else{ + if(svc.local_ids.empty()) { + ep.local_id = ep.claimed_id; + *(oi++) = ep; + }else{ + for(xrd::local_ids_t::const_iterator ilid=svc.local_ids.begin(); + ilid!=svc.local_ids.end(); ++ilid) { + ep.local_id = ilid->second; + *(oi++) = ep; + } + } + } + } + } + } + }; - void idiscover(idiscovery_t& result,const string& identity) { + string idiscover(endpoint_discovery_iterator oi,const string& identity) { idigger_t idigger; - idigger.discover(result,identity); + return idigger.discover(oi,identity); } } diff --git a/lib/extension.cc b/lib/extension.cc index 8f22562..6451249 100644 --- a/lib/extension.cc +++ b/lib/extension.cc @@ -1,15 +1,15 @@ #include <opkele/exception.h> #include <opkele/extension.h> namespace opkele { - void extension_t::checkid_hook(params_t& /* p */,const string& /* identity */ ) { + void extension_t::checkid_hook(basic_openid_message&) { throw not_implemented(OPKELE_CP_ "Consumer checkid_hook not implemented"); } - void extension_t::id_res_hook(const params_t& /* p */,const params_t& /* sp */,const string& /* identity */) { + void extension_t::id_res_hook(const basic_openid_message&,const basic_openid_message&) { throw not_implemented(OPKELE_CP_ "Consumer id_res_hook not implemented"); } - void extension_t::checkid_hook(const params_t& /* pin */,params_t& /* pout */) { + void extension_t::checkid_hook(const basic_openid_message&,basic_openid_message&) { throw not_implemented(OPKELE_CP_ "Server checkid_hook not implemented"); } } diff --git a/lib/extension_chain.cc b/lib/extension_chain.cc index 16537dc..5c2afd9 100644 --- a/lib/extension_chain.cc +++ b/lib/extension_chain.cc @@ -1,16 +1,16 @@ #include <cstdarg> #include <opkele/extension_chain.h> namespace opkele { - void extension_chain_t::checkid_hook(params_t& p,const string& identity) { - for(iterator i=begin();i!=end();++i) (*i)->checkid_hook(p,identity); + void extension_chain_t::checkid_hook(basic_openid_message& om){ + for(iterator i=begin();i!=end();++i) (*i)->checkid_hook(om); } - void extension_chain_t::id_res_hook(const params_t& p,const params_t& sp,const string& identity) { - for(iterator i=begin();i!=end();++i) (*i)->id_res_hook(p,sp,identity); + void extension_chain_t::id_res_hook(const basic_openid_message& om,const basic_openid_message& sp) { + for(iterator i=begin();i!=end();++i) (*i)->id_res_hook(om,sp); } - void extension_chain_t::checkid_hook(const params_t& pin,params_t& pout) { - for(iterator i=begin();i!=end();++i) (*i)->checkid_hook(pin,pout); + void extension_chain_t::checkid_hook(const basic_openid_message& inm,basic_openid_message& oum) { + for(iterator i=begin();i!=end();++i) (*i)->checkid_hook(inm,oum); } } diff --git a/lib/openid_message.cc b/lib/openid_message.cc new file mode 100644 index 0000000..3b08748 --- a/dev/null +++ b/lib/openid_message.cc @@ -0,0 +1,228 @@ +#include <cassert> +#include <opkele/types.h> +#include <opkele/exception.h> +#include <opkele/util.h> +#include <opkele/debug.h> + +#include "config.h" + +namespace opkele { + using std::input_iterator_tag; + using std::unary_function; + + struct __om_copier : public unary_function<const string&,void> { + public: + const basic_openid_message& from; + basic_openid_message& to; + + __om_copier(basic_openid_message& to,const basic_openid_message& from) + : from(from), to(to) { + to.reset_fields(); + } + + result_type operator()(argument_type f) { + to.set_field(f,from.get_field(f)); } + }; + + basic_openid_message::basic_openid_message(const basic_openid_message& x) { + x.copy_to(*this); + } + void basic_openid_message::copy_to(basic_openid_message& x) const { + for_each(fields_begin(),fields_end(), + __om_copier(x,*this) ); + } + + struct __om_ns_finder : public unary_function<const string&,bool> { + public: + const basic_openid_message& om; + const string& uri; + + __om_ns_finder(const basic_openid_message& om, + const string& uri) : om(om), uri(uri) { } + + result_type operator()(argument_type f) { + return + (!strncmp(f.c_str(),"ns.",sizeof("ns.")-1)) + && om.get_field(f)==uri ; + } + }; + + bool basic_openid_message::has_ns(const string& uri) const { + fields_iterator ei = fields_end(); + fields_iterator i = find_if(fields_begin(),fields_end(), + __om_ns_finder(*this,uri)); + return !(i==ei); + } + string basic_openid_message::get_ns(const string& uri) const { + fields_iterator ei = fields_end(); + fields_iterator i = find_if(fields_begin(),fields_end(), + __om_ns_finder(*this,uri)); + if(i==ei) + throw failed_lookup(OPKELE_CP_ string("failed to find namespace ")+uri); + return i->substr(3); + } + + struct __om_query_builder : public unary_function<const string&,void> { + public: + const basic_openid_message& om; + string& rv; + bool first; + + __om_query_builder(string& rv,const basic_openid_message& om) + : om(om), first(true), rv(rv) { + for_each(om.fields_begin(),om.fields_end(),*this); + } + __om_query_builder(string& rv,const basic_openid_message& om,const string& url) + : om(om), first(true), rv(rv) { + rv = url; + if(rv.find('?')==string::npos) + rv += '?'; + else + first = false; + for_each(om.fields_begin(),om.fields_end(),*this); + } + + result_type operator()(argument_type f) { + if(first) + first = false; + else + rv += '&'; + rv += "openid."; rv+= f; + rv += '='; + rv += util::url_encode(om.get_field(f)); + } + }; + + string basic_openid_message::append_query(const string& url) const { + string rv; + return __om_query_builder(rv,*this,url).rv; + } + string basic_openid_message::query_string() const { + string rv; + return __om_query_builder(rv,*this).rv; + } + + void basic_openid_message::reset_fields() { + throw not_implemented(OPKELE_CP_ "reset_fields() not implemented"); + } + void basic_openid_message::set_field(const string& n,const string& v) { + throw not_implemented(OPKELE_CP_ "set_field() not implemented"); + } + void basic_openid_message::reset_field(const string& n) { + throw not_implemented(OPKELE_CP_ "reset_field() not implemented"); + } + + void basic_openid_message::from_keyvalues(const string& kv) { + reset_fields(); + string::size_type p = 0; + while(true) { + string::size_type co = kv.find(':',p); + if(co==string::npos) + break; +#ifndef POSTELS_LAW + string::size_type nl = kv.find('\n',co+1); + if(nl==string::npos) + throw bad_input(OPKELE_CP_ "malformed input"); + if(nl>co) + insert(value_type(kv.substr(p,co-p),kv.substr(co+1,nl-co-1))); + p = nl+1; +#else /* POSTELS_LAW */ + string::size_type lb = kv.find_first_of("\r\n",co+1); + if(lb==string::npos) { + set_field(kv.substr(p,co-p),kv.substr(co+1)); + break; + } + if(lb>co) + set_field(kv.substr(p,co-p),kv.substr(co+1,lb-co-1)); + string::size_type nolb = kv.find_first_not_of("\r\n",lb); + if(nolb==string::npos) + break; + p = nolb; +#endif /* POSTELS_LAW */ + } + } + + void basic_openid_message::add_to_signed(const string& fields) { + string::size_type fnc = fields.find_first_not_of(","); + if(fnc==string::npos) + throw bad_input(OPKELE_CP_ "Trying to add nothing in particular to the list of signed fields"); + string signeds; + try { + signeds = get_field("signed"); + string::size_type lnc = signeds.find_last_not_of(","); + if(lnc==string::npos) + signeds.assign(fields,fnc,fields.size()-fnc); + else{ + string::size_type ss = signeds.size(); + if(lnc==(ss-1)) { + signeds+= ','; + signeds.append(fields,fnc,fields.size()-fnc); + }else{ + if(lnc<(ss-2)) + signeds.replace(lnc+2,ss-lnc-2, + fields,fnc,fields.size()-fnc); + else + signeds.append(fields,fnc,fields.size()-fnc); + } + } + }catch(failed_lookup&) { + signeds.assign(fields,fnc,fields.size()-fnc); + } + set_field("signed",signeds); + } + + string basic_openid_message::find_ns(const string& uri,const char *pfx) const { + if(has_field("ns")) + return get_ns(uri); + return pfx; + } + string basic_openid_message::allocate_ns(const string& uri,const char *pfx) { + if(!has_field("ns")) + return pfx; + if(has_ns(uri)) + throw bad_input(OPKELE_CP_ "OpenID message already contains namespace"); + string rv = pfx; + if(has_field("ns."+rv)) { + string::reference c=rv[rv.length()]; + for(c='a';c<='z' && has_field("ns."+rv);++c); + if(c=='z') + throw exception(OPKELE_CP_ "Failed to allocate namespace"); + } + set_field("ns."+rv,uri); + return rv; + } + + void openid_message_t::copy_to(basic_openid_message& x) const { + x.reset_fields(); + for(const_iterator i=begin();i!=end();++i) + x.set_field(i->first,i->second); + } + + bool openid_message_t::has_field(const string& n) const { + return find(n)!=end(); + } + const string& openid_message_t::get_field(const string& n) const { + const_iterator i=find(n); + if(i==end()) + throw failed_lookup(OPKELE_CP_ n+": no such field"); + return i->second; + } + + openid_message_t::fields_iterator openid_message_t::fields_begin() const { + return util::map_keys_iterator<const_iterator,string,const string&,const string*>(begin(),end()); + } + openid_message_t::fields_iterator openid_message_t::fields_end() const { + return util::map_keys_iterator<const_iterator,string,const string&,const string*>(end(),end()); + } + + void openid_message_t::reset_fields() { + clear(); + } + void openid_message_t::set_field(const string& n,const string& v) { + insert(value_type(n,v)); + } + void openid_message_t::reset_field(const string& n) { + erase(n); + } + +} diff --git a/lib/params.cc b/lib/params.cc index 7a572c1..6805516 100644 --- a/lib/params.cc +++ b/lib/params.cc @@ -1,121 +1,30 @@ #include <opkele/types.h> #include <opkele/exception.h> #include <opkele/util.h> #include <openssl/sha.h> #include <openssl/hmac.h> #include "config.h" namespace opkele { using namespace std; - bool params_t::has_param(const string& n) const { - return find(n)!=end(); - } - const string& params_t::get_param(const string& n) const { - const_iterator i = find(n); - if(i==end()) - throw failed_lookup(OPKELE_CP_ n+": no such parameter"); - return i->second; - } - string& params_t::get_param(const string& n) { - iterator i = find(n); - if(i==end()) - throw failed_lookup(OPKELE_CP_ n+": no such parameter"); - return i->second; - } - - void params_t::parse_keyvalues(const string& kv) { - clear(); - string::size_type p = 0; - while(true) { - string::size_type co = kv.find(':',p); - if(co==string::npos) - break; -#ifndef POSTELS_LAW - string::size_type nl = kv.find('\n',co+1); - if(nl==string::npos) - throw bad_input(OPKELE_CP_ "malformed input"); - if(nl>co) - insert(value_type(kv.substr(p,co-p),kv.substr(co+1,nl-co-1))); - p = nl+1; -#else /* POSTELS_LAW */ - string::size_type lb = kv.find_first_of("\r\n",co+1); - if(lb==string::npos) { - insert(value_type(kv.substr(p,co-p),kv.substr(co+1))); - break; - } - if(lb>co) - insert(value_type(kv.substr(p,co-p),kv.substr(co+1,lb-co-1))); - string::size_type nolb = kv.find_first_not_of("\r\n",lb); - if(nolb==string::npos) - break; - p = nolb; -#endif /* POSTELS_LAW */ - } - } - - void params_t::sign(secret_t secret,string& sig,const string& slist,const char *prefix) const { - string kv; - string::size_type p = 0; - while(true) { - string::size_type co = slist.find(',',p); - string f = (co==string::npos)?slist.substr(p):slist.substr(p,co-p); - kv += f; - kv += ':'; - if(prefix) f.insert(0,prefix); - kv += get_param(f); - kv += '\n'; - if(co==string::npos) - break; - p = co+1; - } - unsigned int md_len = 0; - unsigned char *md = HMAC( - EVP_sha1(), - &(secret.front()),secret.size(), - (const unsigned char *)kv.data(),kv.length(), - 0,&md_len); - sig = util::encode_base64(md,md_len); - } - string params_t::append_query(const string& url,const char *prefix) const { string rv = url; bool p = true; if(rv.find('?')==string::npos) { - rv += '?'; - p = false; - } - for(const_iterator i=begin();i!=end();++i) { + rv += '?'; p = false; } + for(fields_iterator i=fields_begin();i!=fields_end();++i) { if(p) rv += '&'; else p = true; - rv += prefix; - rv += i->first; + if(prefix) rv += prefix; + rv += *i; rv += '='; - rv += util::url_encode(i->second); + rv += util::url_encode(get_field(*i)); } return rv; } - string params_t::query_string(const char *prefix) const { - string rv; - for(const_iterator i=begin();i!=end();++i) { - if(!rv.empty()) - rv += '&'; - rv += prefix; - rv += i->first; - rv += '='; - rv += util::url_encode(i->second); - } - return rv; - } - - ostream& operator << (ostream& o,const params_t& p) { - for(params_t::const_iterator i=p.begin();i!=p.end();++i) - o << i->first << ':' << i->second << '\n'; - return o; - } - } diff --git a/lib/prequeue_rp.cc b/lib/prequeue_rp.cc new file mode 100644 index 0000000..e242f87 --- a/dev/null +++ b/lib/prequeue_rp.cc @@ -0,0 +1,81 @@ +#include <iostream> +#include <openssl/sha.h> +#include <openssl/hmac.h> +#include <opkele/exception.h> +#include <opkele/prequeue_rp.h> +#include <opkele/discovery.h> +#include <opkele/uris.h> +#include <opkele/data.h> +#include <opkele/util.h> +#include <opkele/curl.h> +#include <opkele/debug.h> + +namespace opkele { + + class __OP_verifier_good_input : public exception { + public: + __OP_verifier_good_input(OPKELE_E_PARS) + : exception(OPKELE_E_CONS) { } + }; + + class OP_verifier : public iterator<output_iterator_tag,openid_endpoint_t,void> { + public: + const string& OP; + const string& id; + + OP_verifier(const string& o,const string& i) + : OP(o), id(i) { } + + OP_verifier& operator*() { return *this; } + OP_verifier& operator=(const openid_endpoint_t& oep) { + if(oep.uri==OP) { + if(oep.claimed_id==IDURI_SELECT20 + || oep.local_id==IDURI_SELECT20 ) + throw bad_input(OPKELE_CP_ "claimed_id is an OP-Id"); + if(oep.local_id==id) + throw __OP_verifier_good_input(OPKELE_CP_ "Found corresponding endpoint"); + } + return *this; + } + + OP_verifier& operator++() { return *this; } + OP_verifier& operator++(int) { return *this; } + }; + + void prequeue_RP::verify_OP(const string& OP,const string& claimed_id,const string& identity) const { + try { + idiscover(OP_verifier(OP,identity),claimed_id); + throw id_res_unauthorized(OPKELE_CP_ + "OP is not authorized to make an assertion regarding the identity"); + }catch(__OP_verifier_good_input& ovgi) { + } + } + + class endpoint_queuer : public iterator<output_iterator_tag,openid_endpoint_t,void> { + public: + prequeue_RP& rp; + + endpoint_queuer(prequeue_RP& rp) : rp(rp) { } + + endpoint_queuer& operator*() { return *this; } + endpoint_queuer& operator=(const openid_endpoint_t& oep) { + rp.queue_endpoint(oep); return *this; } + + endpoint_queuer& operator++() { return *this; } + endpoint_queuer& operator++(int) { return *this; } + }; + + void prequeue_RP::initiate(const string& usi) { + begin_queueing(); + set_normalized_id( idiscover(endpoint_queuer(*this),usi) ); + end_queueing(); + } + + void prequeue_RP::set_normalized_id(const string& nid) { + } + + const string prequeue_RP::get_normalized_id() const { + throw not_implemented(OPKELE_CP_ "get_normalized_id() is not implemented"); + } + +} diff --git a/lib/server.cc b/lib/server.cc index 282521e..776f1ae 100644 --- a/lib/server.cc +++ b/lib/server.cc @@ -1,171 +1,171 @@ #include <cstring> #include <vector> #include <openssl/sha.h> #include <openssl/hmac.h> #include <opkele/util.h> #include <opkele/exception.h> #include <opkele/server.h> #include <opkele/data.h> namespace opkele { using namespace std; void server_t::associate(const params_t& pin,params_t& pout) { util::dh_t dh; util::bignum_t c_pub; unsigned char key_sha1[SHA_DIGEST_LENGTH]; enum { sess_cleartext, sess_dh_sha1 } st = sess_cleartext; if( pin.has_param("openid.session_type") && pin.get_param("openid.session_type")=="DH-SHA1" ) { /* TODO: fallback to cleartext in case of exceptions here? */ if(!(dh = DH_new())) throw exception_openssl(OPKELE_CP_ "failed to DH_new()"); c_pub = util::base64_to_bignum(pin.get_param("openid.dh_consumer_public")); if(pin.has_param("openid.dh_modulus")) dh->p = util::base64_to_bignum(pin.get_param("openid.dh_modulus")); else dh->p = util::dec_to_bignum(data::_default_p); if(pin.has_param("openid.dh_gen")) dh->g = util::base64_to_bignum(pin.get_param("openid.dh_gen")); else dh->g = util::dec_to_bignum(data::_default_g); if(!DH_generate_key(dh)) throw exception_openssl(OPKELE_CP_ "failed to DH_generate_key()"); vector<unsigned char> ck(DH_size(dh)+1); unsigned char *ckptr = &(ck.front())+1; int cklen = DH_compute_key(ckptr,c_pub,dh); if(cklen<0) throw exception_openssl(OPKELE_CP_ "failed to DH_compute_key()"); if(cklen && (*ckptr)&0x80) { (*(--ckptr)) = 0; ++cklen; } SHA1(ckptr,cklen,key_sha1); st = sess_dh_sha1; } assoc_t assoc = alloc_assoc(mode_associate); time_t now = time(0); pout.clear(); pout["assoc_type"] = assoc->assoc_type(); pout["assoc_handle"] = assoc->handle(); /* TODO: eventually remove deprecated stuff */ pout["issued"] = util::time_to_w3c(now); pout["expiry"] = util::time_to_w3c(now+assoc->expires_in()); pout["expires_in"] = util::long_to_string(assoc->expires_in()); secret_t secret = assoc->secret(); switch(st) { case sess_dh_sha1: pout["session_type"] = "DH-SHA1"; pout["dh_server_public"] = util::bignum_to_base64(dh->pub_key); secret.enxor_to_base64(key_sha1,pout["enc_mac_key"]); break; default: secret.to_base64(pout["mac_key"]); break; } } void server_t::checkid_immediate(const params_t& pin,string& return_to,params_t& pout,extension_t *ext) { checkid_(mode_checkid_immediate,pin,return_to,pout,ext); } void server_t::checkid_setup(const params_t& pin,string& return_to,params_t& pout,extension_t *ext) { checkid_(mode_checkid_setup,pin,return_to,pout,ext); } void server_t::checkid_(mode_t mode,const params_t& pin,string& return_to,params_t& pout,extension_t *ext) { if(mode!=mode_checkid_immediate && mode!=mode_checkid_setup) throw bad_input(OPKELE_CP_ "invalid checkid_* mode"); pout.clear(); assoc_t assoc; try { assoc = retrieve_assoc(pin.get_param("openid.assoc_handle")); }catch(failed_lookup& fl) { // no handle specified or no valid handle found, going dumb assoc = alloc_assoc(mode_checkid_setup); if(pin.has_param("openid.assoc_handle")) pout["invalidate_handle"]=pin.get_param("openid.assoc_handle"); } string trust_root; try { trust_root = pin.get_param("openid.trust_root"); }catch(failed_lookup& fl) { } string identity = pin.get_param("openid.identity"); return_to = pin.get_param("openid.return_to"); validate(*assoc,pin,identity,trust_root); pout["mode"] = "id_res"; pout["assoc_handle"] = assoc->handle(); if(pin.has_param("openid.assoc_handle") && assoc->stateless()) pout["invalidate_handle"] = pin.get_param("openid.assoc_handle"); pout["identity"] = identity; pout["return_to"] = return_to; /* TODO: eventually remove deprecated stuff */ time_t now = time(0); pout["issued"] = util::time_to_w3c(now); pout["valid_to"] = util::time_to_w3c(now+120); pout["exipres_in"] = "120"; pout["signed"]="mode,identity,return_to"; if(ext) ext->checkid_hook(pin,pout); - pout.sign(assoc->secret(),pout["sig"],pout["signed"]); + pout["sig"] = util::base64_signature(assoc,pout); } void server_t::check_authentication(const params_t& pin,params_t& pout) { vector<unsigned char> sig; const string& sigenc = pin.get_param("openid.sig"); util::decode_base64(sigenc,sig); assoc_t assoc; try { assoc = retrieve_assoc(pin.get_param("openid.assoc_handle")); }catch(failed_lookup& fl) { throw failed_assertion(OPKELE_CP_ "invalid handle or handle not specified"); } if(!assoc->stateless()) throw stateful_handle(OPKELE_CP_ "will not do check_authentication on a stateful handle"); const string& slist = pin.get_param("openid.signed"); string kv; string::size_type p =0; while(true) { string::size_type co = slist.find(',',p); string f = (co==string::npos)?slist.substr(p):slist.substr(p,co-p); kv += f; kv += ':'; if(f=="mode") kv += "id_res"; else { f.insert(0,"openid."); kv += pin.get_param(f); } kv += '\n'; if(co==string::npos) break; p = co+1; } secret_t secret = assoc->secret(); unsigned int md_len = 0; unsigned char *md = HMAC( EVP_sha1(), &(secret.front()),secret.size(), (const unsigned char *)kv.data(),kv.length(), 0,&md_len); pout.clear(); if(sig.size()==md_len && !memcmp(&(sig.front()),md,md_len)) { pout["is_valid"]="true"; pout["lifetime"]="60"; /* TODO: eventually remove deprecated stuff */ }else{ pout["is_valid"]="false"; pout["lifetime"]="0"; /* TODO: eventually remove deprecated stuff */ } if(pin.has_param("openid.invalidate_handle")) { string h = pin.get_param("openid.invalidate_handle"); try { assoc_t tmp = retrieve_assoc(h); }catch(invalid_handle& ih) { pout["invalidate_handle"] = h; }catch(failed_lookup& fl) { } } } } diff --git a/lib/sreg.cc b/lib/sreg.cc index 03edf57..7e2d588 100644 --- a/lib/sreg.cc +++ b/lib/sreg.cc @@ -1,126 +1,140 @@ #include <opkele/exception.h> #include <opkele/sreg.h> #include <opkele/uris.h> #include <algorithm> namespace opkele { using std::find; static const struct _sreg_field { const char *fieldname; sreg_t::fieldbit_t fieldbit; } fields[] = { { "nickname", sreg_t::field_nickname }, { "email", sreg_t::field_email }, { "fullname", sreg_t::field_fullname }, { "dob", sreg_t::field_dob }, { "gender", sreg_t::field_gender }, { "postcode", sreg_t::field_postcode }, { "country", sreg_t::field_country }, { "language", sreg_t::field_language }, { "timezone", sreg_t::field_timezone } }; # define fields_BEGIN fields # define fields_END &fields[sizeof(fields)/sizeof(*fields)] typedef const struct _sreg_field *fields_iterator; bool operator==(const struct _sreg_field& fd,const string& fn) { return fd.fieldname==fn; } - void sreg_t::checkid_hook(params_t& p,const string& /* identity */) { + void sreg_t::checkid_hook(basic_openid_message& om) { string fr, fo; for(fields_iterator f=fields_BEGIN;f<fields_END;++f) { if(f->fieldbit&fields_required) { if(!fr.empty()) fr+=","; fr += f->fieldname; } if(f->fieldbit&fields_optional) { if(!fo.empty()) fo+=","; fo += f->fieldname; } } - p["ns.sreg"] = OIURI_SREG11; - if(!fr.empty()) p["sreg.required"]=fr; - if(!fo.empty()) p["sreg.optional"]=fo; - if(!policy_url.empty()) p["sreg.policy_url"]=policy_url; + string pfx = om.allocate_ns(OIURI_SREG11,"sreg"); + if(!fr.empty()) om.set_field(pfx+".required",fr); + if(!fo.empty()) om.set_field(pfx+".optional",fo); + if(!policy_url.empty()) om.set_field(pfx+".policy_url",policy_url); } - void sreg_t::id_res_hook(const params_t& /* p */,const params_t& sp,const string& /* identity */) { + void sreg_t::id_res_hook(const basic_openid_message& om,const basic_openid_message& sp) { clear(); + string pfx; + try { + pfx = om.find_ns(OIURI_SREG11,"sreg"); + }catch(failed_lookup& fl) { + try { + pfx = om.find_ns(OIURI_SREG10,"sreg"); + }catch(failed_lookup& fl) { + return; + } + } + pfx += '.'; for(fields_iterator f=fields_BEGIN;f<fields_END;++f) { - string fn = "sreg."; fn+=f->fieldname; - if(!sp.has_param(fn)) continue; + string fn = pfx; fn+=f->fieldname; + if(!sp.has_field(fn)) continue; has_fields |= f->fieldbit; - response[f->fieldbit]=sp.get_param(fn); + response[f->fieldbit]=sp.get_field(fn); } } const string& sreg_t::get_field(fieldbit_t fb) const { response_t::const_iterator i = response.find(fb); if(i==response.end()) throw failed_lookup(OPKELE_CP_ "no field data available"); return i->second; } void sreg_t::set_field(fieldbit_t fb,const string& fv) { response[fb] = fv; has_fields |= fb; } void sreg_t::reset_field(fieldbit_t fb) { has_fields &= ~fb; response.erase(fb); } void sreg_t::clear() { has_fields = 0; response.clear(); } static long fields_list_to_bitmask(string& fl) { long rv = 0; while(!fl.empty()) { string::size_type co = fl.find(','); string fn; if(co==string::npos) { fn = fl; fl.erase(); }else{ fn = fl.substr(0,co); fl.erase(0,co+1); } fields_iterator f = find(fields_BEGIN,fields_END,fn); if(f!=fields_END) rv |= f->fieldbit; } return rv; } - void sreg_t::checkid_hook(const params_t& pin,params_t& pout) { + void sreg_t::checkid_hook(const basic_openid_message& inm,basic_openid_message& oum) { + string ins = inm.find_ns(OIURI_SREG11,"sreg"); fields_optional = 0; fields_required = 0; policy_url.erase(); fields_response = 0; try { - string fl = pin.get_param("openid.sreg.required"); + string fl = inm.get_field(ins+".required"); fields_required = fields_list_to_bitmask(fl); }catch(failed_lookup&) { } try { - string fl = pin.get_param("openid.sreg.optional"); + string fl = inm.get_field(ins+".optional"); fields_optional = fields_list_to_bitmask(fl); }catch(failed_lookup&) { } try { - policy_url = pin.get_param("openid.sreg.policy_url"); + policy_url = inm.get_field(ins+".policy_url"); }catch(failed_lookup&) { } - setup_response(pin,pout); + setup_response(inm,oum); + string ons = oum.allocate_ns(OIURI_SREG11,"sreg"); fields_response &= has_fields; + string signeds = "ns."+ons; for(fields_iterator f=fields_BEGIN;f<fields_END;++f) { if(!(f->fieldbit&fields_response)) continue; - if(!pout["signed"].empty()) - pout["signed"] +=','; - string pn = "sreg."; pn += f->fieldname; - pout["signed"] += pn; - pout[pn] = get_field(f->fieldbit); + signeds +=','; + string pn = ons; pn += '.'; pn += f->fieldname; + signeds += pn; + oum.set_field(pn,get_field(f->fieldbit)); } + oum.add_to_signed(signeds); } - void sreg_t::setup_response(const params_t& /* pin */,params_t& /* pout */) { + void sreg_t::setup_response(const basic_openid_message& /* inm */,basic_openid_message& /* oum */) { fields_response = (fields_required|fields_optional)&has_fields; } } diff --git a/lib/util.cc b/lib/util.cc index a9b9bed..54d6535 100644 --- a/lib/util.cc +++ b/lib/util.cc @@ -1,316 +1,383 @@ #include <errno.h> #include <cassert> #include <cctype> #include <cstring> #include <vector> #include <string> #include <stack> #include <openssl/bio.h> #include <openssl/evp.h> +#include <openssl/hmac.h> #include <curl/curl.h> #include "opkele/util.h" #include "opkele/exception.h" +#include <config.h> +#ifdef HAVE_DEMANGLE +# include <cxxabi.h> +#endif + namespace opkele { using namespace std; namespace util { /* * base64 */ string encode_base64(const void *data,size_t length) { BIO *b64 = 0, *bmem = 0; try { b64 = BIO_new(BIO_f_base64()); if(!b64) throw exception_openssl(OPKELE_CP_ "failed to BIO_new() base64 encoder"); BIO_set_flags(b64,BIO_FLAGS_BASE64_NO_NL); bmem = BIO_new(BIO_s_mem()); BIO_set_flags(b64,BIO_CLOSE); if(!bmem) throw exception_openssl(OPKELE_CP_ "failed to BIO_new() memory buffer"); BIO_push(b64,bmem); if(((size_t)BIO_write(b64,data,length))!=length) throw exception_openssl(OPKELE_CP_ "failed to BIO_write()"); if(BIO_flush(b64)!=1) throw exception_openssl(OPKELE_CP_ "failed to BIO_flush()"); char *rvd; long rvl = BIO_get_mem_data(bmem,&rvd); string rv(rvd,rvl); BIO_free_all(b64); return rv; }catch(...) { if(b64) BIO_free_all(b64); throw; } } void decode_base64(const string& data,vector<unsigned char>& rv) { BIO *b64 = 0, *bmem = 0; rv.clear(); try { bmem = BIO_new_mem_buf((void*)data.data(),data.size()); if(!bmem) throw exception_openssl(OPKELE_CP_ "failed to BIO_new_mem_buf()"); b64 = BIO_new(BIO_f_base64()); if(!b64) throw exception_openssl(OPKELE_CP_ "failed to BIO_new() base64 decoder"); BIO_set_flags(b64,BIO_FLAGS_BASE64_NO_NL); BIO_push(b64,bmem); unsigned char tmp[512]; size_t rb = 0; while((rb=BIO_read(b64,tmp,sizeof(tmp)))>0) rv.insert(rv.end(),tmp,&tmp[rb]); BIO_free_all(b64); }catch(...) { if(b64) BIO_free_all(b64); throw; } } /* * big numerics */ BIGNUM *base64_to_bignum(const string& b64) { vector<unsigned char> bin; decode_base64(b64,bin); BIGNUM *rv = BN_bin2bn(&(bin.front()),bin.size(),0); if(!rv) throw failed_conversion(OPKELE_CP_ "failed to BN_bin2bn()"); return rv; } BIGNUM *dec_to_bignum(const string& dec) { BIGNUM *rv = 0; if(!BN_dec2bn(&rv,dec.c_str())) throw failed_conversion(OPKELE_CP_ "failed to BN_dec2bn()"); return rv; } string bignum_to_base64(const BIGNUM *bn) { vector<unsigned char> bin(BN_num_bytes(bn)+1); unsigned char *binptr = &(bin.front())+1; int l = BN_bn2bin(bn,binptr); if(l && (*binptr)&0x80){ (*(--binptr)) = 0; ++l; } return encode_base64(binptr,l); } /* * w3c times */ string time_to_w3c(time_t t) { struct tm tm_t; if(!gmtime_r(&t,&tm_t)) throw failed_conversion(OPKELE_CP_ "failed to BN_dec2bn()"); char rv[25]; if(!strftime(rv,sizeof(rv)-1,"%Y-%m-%dT%H:%M:%SZ",&tm_t)) throw failed_conversion(OPKELE_CP_ "failed to strftime()"); return rv; } time_t w3c_to_time(const string& w) { int fraction; struct tm tm_t; memset(&tm_t,0,sizeof(tm_t)); if( ( sscanf( w.c_str(), "%04d-%02d-%02dT%02d:%02d:%02dZ", &tm_t.tm_year,&tm_t.tm_mon,&tm_t.tm_mday, &tm_t.tm_hour,&tm_t.tm_min,&tm_t.tm_sec ) != 6 ) && ( sscanf( w.c_str(), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", &tm_t.tm_year,&tm_t.tm_mon,&tm_t.tm_mday, &tm_t.tm_hour,&tm_t.tm_min,&tm_t.tm_sec, &fraction ) != 7 ) ) throw failed_conversion(OPKELE_CP_ "failed to sscanf()"); tm_t.tm_mon--; tm_t.tm_year-=1900; time_t rv = mktime(&tm_t); if(rv==(time_t)-1) throw failed_conversion(OPKELE_CP_ "failed to mktime()"); return rv-timezone; } /* * */ string url_encode(const string& str) { char * t = curl_escape(str.c_str(),str.length()); if(!t) throw failed_conversion(OPKELE_CP_ "failed to curl_escape()"); string rv(t); curl_free(t); return rv; } string long_to_string(long l) { char rv[32]; int r=snprintf(rv,sizeof(rv),"%ld",l); if(r<0 || r>=(int)sizeof(rv)) throw failed_conversion(OPKELE_CP_ "failed to snprintf()"); return rv; } long string_to_long(const string& s) { char *endptr = 0; long rv = strtol(s.c_str(),&endptr,10); if((!endptr) || endptr==s.c_str()) throw failed_conversion(OPKELE_CP_ "failed to strtol()"); return rv; } /* * Normalize URL according to the rules, described in rfc 3986, section 6 * * - uppercase hex triplets (e.g. %ab -> %AB) * - lowercase scheme and host * - decode %-encoded characters, specified as unreserved in rfc 3986, section 2.3, * that is - [:alpha:][:digit:]._~- * - remove dot segments * - remove empty and default ports * - if there's no path component, add '/' */ string rfc_3986_normalize_uri(const string& uri) { static const char *whitespace = " \t\r\n"; string rv; string::size_type ns = uri.find_first_not_of(whitespace); if(ns==string::npos) throw bad_input(OPKELE_CP_ "Can't normalize empty URI"); string::size_type colon = uri.find(':',ns); if(colon==string::npos) throw bad_input(OPKELE_CP_ "No scheme specified in URI"); transform( uri.begin()+ns, uri.begin()+colon+1, back_inserter(rv), ::tolower ); bool s; string::size_type ul = uri.find_last_not_of(whitespace)+1; if(ul <= (colon+3)) throw bad_input(OPKELE_CP_ "Unexpected end of URI being normalized encountered"); if(uri[colon+1]!='/' || uri[colon+2]!='/') throw bad_input(OPKELE_CP_ "Unexpected input in URI being normalized after scheme component"); if(rv=="http:") s = false; else if(rv=="https:") s = true; else{ - /* TODO: support more schemes. - * e.g. xri. How do we normalize + /* TODO: support more schemes. e.g. xri. How do we normalize * xri? */ rv.append(uri,colon+1,ul-colon-1); return rv; } rv += "//"; string::size_type interesting = uri.find_first_of(":/#?",colon+3); if(interesting==string::npos) { transform( uri.begin()+colon+3,uri.begin()+ul, back_inserter(rv), ::tolower ); rv += '/'; return rv; } transform( uri.begin()+colon+3,uri.begin()+interesting, back_inserter(rv), ::tolower ); bool qf = false; char ic = uri[interesting]; if(ic==':') { string::size_type ni = uri.find_first_of("/#?%",interesting+1); const char *nptr = uri.data()+interesting+1; char *eptr = 0; long port = strtol(nptr,&eptr,10); if( (port>0) && (port<65535) && port!=(s?443:80) ) { char tmp[8]; snprintf(tmp,sizeof(tmp),":%ld",port); rv += tmp; } if(ni==string::npos) { rv += '/'; return rv; } interesting = ni; }else if(ic!='/') { rv += '/'; rv += ic; qf = true; ++interesting; } string::size_type n = interesting; char tmp[3] = { 0,0,0 }; stack<string::size_type> psegs; psegs.push(rv.length()); string pseg; for(;n<ul;) { string::size_type unsafe = uri.find_first_of(qf?"%":"%/?#",n); if(unsafe==string::npos) { pseg.append(uri,n,ul-n-1); n = ul-1; }else{ pseg.append(uri,n,unsafe-n); n = unsafe; } char c = uri[n++]; if(c=='%') { if((n+1)>=ul) throw bad_input(OPKELE_CP_ "Unexpected end of URI encountered while parsing percent-encoded character"); tmp[0] = uri[n++]; tmp[1] = uri[n++]; if(!( isxdigit(tmp[0]) && isxdigit(tmp[1]) )) throw bad_input(OPKELE_CP_ "Invalid percent-encoded character in URI being normalized"); int cc = strtol(tmp,0,16); if( isalpha(cc) || isdigit(cc) || strchr("._~-",cc) ) pseg += cc; else{ pseg += '%'; pseg += toupper(tmp[0]); pseg += toupper(tmp[1]); } }else if(qf) { rv += pseg; rv += c; pseg.clear(); }else if(n>=ul || strchr("?/#",c)) { if(pseg.empty() || pseg==".") { }else if(pseg=="..") { if(psegs.size()>1) { rv.resize(psegs.top()); psegs.pop(); } }else{ psegs.push(rv.length()); if(c!='/') { pseg += c; qf = true; } rv += '/'; rv += pseg; } if(c=='/' && (n>=ul || strchr("?#",uri[n])) ) { rv += '/'; if(n<ul) qf = true; }else if(strchr("?#",c)) { if(psegs.size()==1 && psegs.top()==rv.length()) rv += '/'; if(pseg.empty()) rv += c; qf = true; } pseg.clear(); }else{ pseg += c; } } if(!pseg.empty()) { if(!qf) rv += '/'; rv += pseg; } return rv; } + string& strip_uri_fragment_part(string& u) { + string::size_type q = u.find('?'), f = u.find('#'); + if(q==string::npos) { + if(f!=string::npos) + u.erase(f); + }else{ + if(f!=string::npos) { + if(f<q) + u.erase(f,q-f); + else + u.erase(f); + } + } + return u; + } + + string abi_demangle(const char *mn) { +#ifndef HAVE_DEMANGLE + return mn; +#else /* !HAVE_DEMANGLE */ + int dstat; + char *demangled = abi::__cxa_demangle(mn,0,0,&dstat); + if(dstat) + return mn; + string rv = demangled; + free(demangled); + return rv; +#endif /* !HAVE_DEMANGLE */ + } + + string base64_signature(const assoc_t& assoc,const basic_openid_message& om) { + const string& slist = om.get_field("signed"); + string kv; + string::size_type p=0; + while(true) { + string::size_type co = slist.find(',',p); + string f = (co==string::npos) + ?slist.substr(p):slist.substr(p,co-p); + kv += f; + kv += ':'; + kv += om.get_field(f); + kv += '\n'; + if(co==string::npos) break; + p = co+1; + } + const secret_t& secret = assoc->secret(); + const EVP_MD *evpmd; + const string& at = assoc->assoc_type(); + if(at=="HMAC-SHA256") + evpmd = EVP_sha256(); + else if(at=="HMAC-SHA1") + evpmd = EVP_sha1(); + else + throw unsupported(OPKELE_CP_ "unknown association type"); + unsigned int md_len = 0; + unsigned char *md = HMAC(evpmd, + &(secret.front()),secret.size(), + (const unsigned char*)kv.data(),kv.length(), + 0,&md_len); + return encode_base64(md,md_len); + } + } } |