author | Michael Krelin <hacker@klever.net> | 2008-02-19 10:52:09 (UTC) |
---|---|---|
committer | Michael Krelin <hacker@klever.net> | 2008-02-19 10:52:09 (UTC) |
commit | 42e4fb613d190508b3e8b8993d233044eeea4d20 (patch) (side-by-side diff) | |
tree | 9b8ebc420942554f927a777e03c70a7c65305a88 | |
parent | a3db32747e8370cab8cfdcc382fee875613b7b77 (diff) | |
download | libopkele-42e4fb613d190508b3e8b8993d233044eeea4d20.zip libopkele-42e4fb613d190508b3e8b8993d233044eeea4d20.tar.gz libopkele-42e4fb613d190508b3e8b8993d233044eeea4d20.tar.bz2 |
basic_RP: add methods for accessing identity information passed from OP.
Signed-off-by: Michael Krelin <hacker@klever.net>
-rw-r--r-- | include/opkele/basic_rp.h | 36 | ||||
-rw-r--r-- | lib/basic_rp.cc | 29 |
2 files changed, 63 insertions, 2 deletions
diff --git a/include/opkele/basic_rp.h b/include/opkele/basic_rp.h index d5356aa..d096e0a 100644 --- a/include/opkele/basic_rp.h +++ b/include/opkele/basic_rp.h @@ -1,200 +1,236 @@ #ifndef __OPKELE_BASIC_RP_H #define __OPKELE_BASIC_RP_H #include <string> #include <opkele/types.h> #include <opkele/extension.h> namespace opkele { using std::string; class basic_RP { public: + /** + * Claimed identifier from a parsed id_res message. + */ + string claimed_id; + /** + * OP-Local identifier from a parsed id_res message. + */ + string identity; virtual ~basic_RP() { } + void reset_vars(); + + /** + * @name Assertion information retrieval + * Retrieval of the information passed with openid message + * @{ + */ + /** + * Find out if the assertion is about identity + * @return true if so + */ + bool has_identity() const; + /** + * Get claimed identifier supplied with the request + * @return claimed identifier + * @throw non_identity if request is not about identity + */ + const string& get_claimed_id() const; + /** + * Get the identity (OP-Local identifier) confirmed + * @return identity + * @throw non_identity if request is not about identity + */ + const string& get_identity() const; + /** + * @} + */ + /** * @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/lib/basic_rp.cc b/lib/basic_rp.cc index e65d9fb..3357d0b 100644 --- a/lib/basic_rp.cc +++ b/lib/basic_rp.cc @@ -1,300 +1,325 @@ #include <cassert> #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/util-internal.h> #include <opkele/curl.h> +#include <opkele/debug.h> namespace opkele { + void basic_RP::reset_vars() { + claimed_id.clear(); identity.clear(); + } + + const string& basic_RP::get_claimed_id() const { + if(claimed_id.empty()) + throw non_identity(OPKELE_CP_ "attempting to retreive claimed_id of non-identity assertion"); + assert(!identity.empty()); + return claimed_id; + } + + const string& basic_RP::get_identity() const { + if(identity.empty()) + throw non_identity(OPKELE_CP_ "attempting to retrieve identity of non-identity related assertion"); + assert(!claimed_id.empty()); + return identity; + } + 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*), size_t exp_s_len) 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; } assert(d_len<=SHA256_DIGEST_LENGTH); unsigned char key_digest[SHA256_DIGEST_LENGTH]; secret.enxor_from_base64((*d_fun)(ckptr,cklen,key_digest),om.get_field("enc_mac_key")); if(secret.size()!=exp_s_len) throw bad_input(OPKELE_CP_ "Secret length isn't consistent with association type"); }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, SHA256_DIGEST_LENGTH ); expires_in = util::string_to_long(res.get_field("expires_in")); }catch(exception&) { 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, SHA_DIGEST_LENGTH ); expires_in = util::string_to_long(res.get_field("expires_in")); }catch(bad_input&) { 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->rp_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) { + reset_vars(); 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(size_t 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")) { + claimed_id = om.get_field("claimed_id"); + identity = om.get_field("identity"); verify_OP( om.get_field("op_endpoint"), - om.get_field("claimed_id"), - om.get_field("identity") ); + claimed_id, identity ); } + }else{ + claimed_id = get_endpoint().claimed_id; + /* TODO: check if this is the identity we asked for */ + identity = om.get_field("identity"); } if(ext) ext->rp_id_res_hook(om,signeds); } void basic_RP::check_authentication(const string& OP, const basic_openid_message& om){ openid_message_t res; static const string checkauthmode = "check_authentication"; direct_request(res,util::change_mode_message_proxy(om,checkauthmode),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"); } } |