-rw-r--r-- | lib/consumer.cc | 316 |
1 files changed, 316 insertions, 0 deletions
diff --git a/lib/consumer.cc b/lib/consumer.cc new file mode 100644 index 0000000..bd76b61 --- a/dev/null +++ b/lib/consumer.cc | |||
@@ -0,0 +1,316 @@ | |||
1 | #include <algorithm> | ||
2 | #include <opkele/util.h> | ||
3 | #include <opkele/exception.h> | ||
4 | #include <opkele/data.h> | ||
5 | #include <opkele/consumer.h> | ||
6 | #include <openssl/sha.h> | ||
7 | #include <openssl/hmac.h> | ||
8 | #include <mimetic/mimetic.h> | ||
9 | #include <curl/curl.h> | ||
10 | #include <pcre++.h> | ||
11 | |||
12 | #include <iostream> | ||
13 | |||
14 | /* silly mimetic */ | ||
15 | #undef PACKAGE | ||
16 | #undef PACKAGE_BUGREPORT | ||
17 | #undef PACKAGE_NAME | ||
18 | #undef PACKAGE_STRING | ||
19 | #undef PACKAGE_TARNAME | ||
20 | #undef PACKAGE_VERSION | ||
21 | #undef VERSION | ||
22 | |||
23 | #include "config.h" | ||
24 | |||
25 | namespace opkele { | ||
26 | using namespace std; | ||
27 | |||
28 | class curl_t { | ||
29 | public: | ||
30 | CURL *_c; | ||
31 | |||
32 | curl_t() : _c(0) { } | ||
33 | curl_t(CURL *c) : _c(c) { } | ||
34 | ~curl_t() throw() { if(_c) curl_easy_cleanup(_c); } | ||
35 | |||
36 | curl_t& operator=(CURL *c) { if(_c) curl_easy_cleanup(_c); _c=c; return *this; } | ||
37 | |||
38 | operator const CURL*(void) const { return _c; } | ||
39 | operator CURL*(void) { return _c; } | ||
40 | }; | ||
41 | |||
42 | static CURLcode curl_misc_sets(CURL* c) { | ||
43 | CURLcode r; | ||
44 | (r=curl_easy_setopt(c,CURLOPT_FOLLOWLOCATION,1)) | ||
45 | || (r=curl_easy_setopt(c,CURLOPT_MAXREDIRS,5)) | ||
46 | || (r=curl_easy_setopt(c,CURLOPT_DNS_CACHE_TIMEOUT,120)) | ||
47 | || (r=curl_easy_setopt(c,CURLOPT_DNS_USE_GLOBAL_CACHE,1)) | ||
48 | || (r=curl_easy_setopt(c,CURLOPT_USERAGENT,PACKAGE_NAME"/"PACKAGE_VERSION)) | ||
49 | || (r=curl_easy_setopt(c,CURLOPT_TIMEOUT,20)) | ||
50 | ; | ||
51 | return r; | ||
52 | } | ||
53 | |||
54 | static size_t _curl_tostring(void *ptr,size_t size,size_t nmemb,void *stream) { | ||
55 | string *str = (string*)stream; | ||
56 | size_t bytes = size*nmemb; | ||
57 | size_t get = min(16384-str->length(),bytes); | ||
58 | str->append((const char*)ptr,get); | ||
59 | return get; | ||
60 | } | ||
61 | |||
62 | assoc_t consumer_t::associate(const string& server) { | ||
63 | util::dh_t dh = DH_new(); | ||
64 | if(!dh) | ||
65 | throw exception_openssl(OPKELE_CP_ "failed to DH_new()"); | ||
66 | dh->p = util::dec_to_bignum(data::_default_p); | ||
67 | dh->g = util::dec_to_bignum(data::_default_g); | ||
68 | if(!DH_generate_key(dh)) | ||
69 | throw exception_openssl(OPKELE_CP_ "failed to DH_generate_key()"); | ||
70 | string request = | ||
71 | "openid.mode=associate" | ||
72 | "&openid.assoc_type=HMAC-SHA1" | ||
73 | "&openid.session_type=DH-SHA1" | ||
74 | "&openid.dh_consumer_public="; | ||
75 | request += util::url_encode(util::bignum_to_base64(dh->pub_key)); | ||
76 | curl_t curl = curl_easy_init(); | ||
77 | if(!curl) | ||
78 | throw exception_curl(OPKELE_CP_ "failed to curl_easy_init()"); | ||
79 | string response; | ||
80 | CURLcode r; | ||
81 | (r=curl_misc_sets(curl)) | ||
82 | || (r=curl_easy_setopt(curl,CURLOPT_URL,server.c_str())) | ||
83 | || (r=curl_easy_setopt(curl,CURLOPT_POST,1)) | ||
84 | || (r=curl_easy_setopt(curl,CURLOPT_POSTFIELDS,request.data())) | ||
85 | || (r=curl_easy_setopt(curl,CURLOPT_POSTFIELDSIZE,request.length())) | ||
86 | || (r=curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,_curl_tostring)) | ||
87 | || (r=curl_easy_setopt(curl,CURLOPT_WRITEDATA,&response)) | ||
88 | ; | ||
89 | if(r) | ||
90 | throw exception_curl(OPKELE_CP_ "failed to curl_easy_setopt()",r); | ||
91 | if(r=curl_easy_perform(curl)) | ||
92 | throw exception_curl(OPKELE_CP_ "failed to curl_easy_perform()",r); | ||
93 | params_t p; p.parse_keyvalues(response); | ||
94 | if(p.has_param("assoc_type") && p.get_param("assoc_type")!="HMAC-SHA1") | ||
95 | throw bad_input(OPKELE_CP_ "unsupported assoc_type"); | ||
96 | string st; | ||
97 | if(p.has_param("session_type")) st = p.get_param("session_type"); | ||
98 | if((!st.empty()) && st!="DH-SHA1") | ||
99 | throw bad_input(OPKELE_CP_ "unsupported session_type"); | ||
100 | secret_t secret; | ||
101 | if(st.empty()) { | ||
102 | secret.from_base64(p.get_param("mac_key")); | ||
103 | }else{ | ||
104 | util::bignum_t s_pub = util::base64_to_bignum(p.get_param("dh_server_public")); | ||
105 | vector<unsigned char> ck(DH_size(dh)); | ||
106 | int cklen = DH_compute_key(&(ck.front()),s_pub,dh); | ||
107 | if(cklen<0) | ||
108 | throw exception_openssl(OPKELE_CP_ "failed to DH_compute_key()"); | ||
109 | ck.resize(cklen); | ||
110 | // OpenID algorithm requires extra zero in case of set bit here | ||
111 | if(ck[0]&0x80) ck.insert(ck.begin(),1,0); | ||
112 | unsigned char key_sha1[SHA_DIGEST_LENGTH]; | ||
113 | SHA1(&(ck.front()),ck.size(),key_sha1); | ||
114 | secret.enxor_from_base64(key_sha1,p.get_param("enc_mac_key")); | ||
115 | } | ||
116 | int expires_in = 0; | ||
117 | if(p.has_param("expires_in")) { | ||
118 | expires_in = util::string_to_long(p.get_param("expires_in")); | ||
119 | }else if(p.has_param("issued") && p.has_param("expiry")) { | ||
120 | expires_in = util::w3c_to_time(p.get_param("expiry"))-util::w3c_to_time(p.get_param("issued")); | ||
121 | }else | ||
122 | throw bad_input(OPKELE_CP_ "no expiration information"); | ||
123 | return store_assoc(server,p.get_param("assoc_handle"),secret,expires_in); | ||
124 | } | ||
125 | |||
126 | string consumer_t::checkid_immediate(const string& identity,const string& return_to,const string& trust_root) { | ||
127 | return checkid_(mode_checkid_immediate,identity,return_to,trust_root); | ||
128 | } | ||
129 | string consumer_t::checkid_setup(const string& identity,const string& return_to,const string& trust_root) { | ||
130 | return checkid_(mode_checkid_setup,identity,return_to,trust_root); | ||
131 | } | ||
132 | string consumer_t::checkid_(mode_t mode,const string& identity,const string& return_to,const string& trust_root) { | ||
133 | params_t p; | ||
134 | if(mode==mode_checkid_immediate) | ||
135 | p["mode"]="checkid_immediate"; | ||
136 | else if(mode==mode_checkid_setup) | ||
137 | p["mode"]="checkid_setup"; | ||
138 | else | ||
139 | throw bad_input(OPKELE_CP_ "unknown checkid_* mode"); | ||
140 | string iurl = util::canonicalize_url(identity); | ||
141 | string server, delegate; | ||
142 | retrieve_links(iurl,server,delegate); | ||
143 | p["identity"] = delegate.empty()?iurl:delegate; | ||
144 | if(!trust_root.empty()) | ||
145 | p["trust_root"] = trust_root; | ||
146 | p["return_to"] = return_to; | ||
147 | try { | ||
148 | try { | ||
149 | string ah = find_assoc(server)->handle(); | ||
150 | p["assoc_handle"] = ah; | ||
151 | }catch(failed_lookup& fl) { | ||
152 | string ah = associate(server)->handle(); | ||
153 | p["assoc_handle"] = ah; | ||
154 | } | ||
155 | }catch(exception& e) { } | ||
156 | return p.append_query(server); | ||
157 | } | ||
158 | |||
159 | void consumer_t::id_res(const params_t& pin,const string& identity) { | ||
160 | if(pin.has_param("openid.user_setup_url")) | ||
161 | throw id_res_setup(OPKELE_CP_ "assertion failed, setup url provided",pin.get_param("openid.user_setup_url")); | ||
162 | string server,delegate; | ||
163 | retrieve_links(identity.empty()?pin.get_param("openid.identity"):util::canonicalize_url(identity),server,delegate); | ||
164 | try { | ||
165 | assoc_t assoc = retrieve_assoc(server,pin.get_param("openid.assoc_handle")); | ||
166 | const string& sigenc = pin.get_param("openid.sig"); | ||
167 | mimetic::Base64::Decoder b; | ||
168 | vector<unsigned char> sig; | ||
169 | mimetic::decode( | ||
170 | sigenc.begin(),sigenc.end(), b, | ||
171 | back_insert_iterator<vector<unsigned char> >(sig) ); | ||
172 | const string& slist = pin.get_param("openid.signed"); | ||
173 | string kv; | ||
174 | string::size_type p = 0; | ||
175 | while(true) { | ||
176 | string::size_type co = slist.find(',',p); | ||
177 | string f = (co==string::npos)?slist.substr(p):slist.substr(p,co-p); | ||
178 | kv += f; | ||
179 | kv += ':'; | ||
180 | f.insert(0,"openid."); | ||
181 | kv += pin.get_param(f); | ||
182 | kv += '\n'; | ||
183 | if(co==string::npos) | ||
184 | break; | ||
185 | p = co+1; | ||
186 | } | ||
187 | secret_t secret = assoc->secret(); | ||
188 | unsigned int md_len = 0; | ||
189 | unsigned char *md = HMAC( | ||
190 | EVP_sha1(), | ||
191 | &(secret.front()),secret.size(), | ||
192 | (const unsigned char *)kv.data(),kv.length(), | ||
193 | 0,&md_len); | ||
194 | if(sig.size()!=md_len || memcmp(&(sig.front()),md,md_len)) | ||
195 | throw id_res_mismatch(OPKELE_CP_ "signature mismatch"); | ||
196 | }catch(failed_lookup& e) { /* XXX: more specific? */ | ||
197 | const string& slist = pin.get_param("openid.signed"); | ||
198 | string::size_type pp = 0; | ||
199 | params_t p; | ||
200 | while(true) { | ||
201 | string::size_type co = slist.find(',',pp); | ||
202 | string f = "openid."; | ||
203 | f += (co==string::npos)?slist.substr(pp):slist.substr(pp,co-pp); | ||
204 | p[f] = pin.get_param(f); | ||
205 | if(co==string::npos) | ||
206 | break; | ||
207 | pp = co+1; | ||
208 | } | ||
209 | p["openid.assoc_handle"] = pin.get_param("openid.assoc_handle"); | ||
210 | p["openid.sig"] = pin.get_param("openid.sig"); | ||
211 | p["openid.signed"] = pin.get_param("openid.signed"); | ||
212 | try { | ||
213 | string ih = pin.get_param("openid.invalidate_handle"); | ||
214 | p["openid.invalidate_handle"] = ih; | ||
215 | }catch(failed_lookup& fl) { } | ||
216 | try { | ||
217 | check_authentication(server,p); | ||
218 | }catch(failed_check_authentication& fca) { | ||
219 | throw id_res_failed(OPKELE_CP_ "failed to check_authentication()"); | ||
220 | } | ||
221 | } | ||
222 | } | ||
223 | |||
224 | void consumer_t::check_authentication(const string& server,const params_t& p) { | ||
225 | string request = "openid.mode=check_authentication"; | ||
226 | for(params_t::const_iterator i=p.begin();i!=p.end();++i) { | ||
227 | if(i->first!="openid.mode") { | ||
228 | request += '&'; | ||
229 | request += i->first; | ||
230 | request += '='; | ||
231 | request += util::url_encode(i->second); | ||
232 | } | ||
233 | } | ||
234 | curl_t curl = curl_easy_init(); | ||
235 | if(!curl) | ||
236 | throw exception_curl(OPKELE_CP_ "failed to curl_easy_init()"); | ||
237 | string response; | ||
238 | CURLcode r; | ||
239 | (r=curl_misc_sets(curl)) | ||
240 | || (r=curl_easy_setopt(curl,CURLOPT_URL,server.c_str())) | ||
241 | || (r=curl_easy_setopt(curl,CURLOPT_POST,1)) | ||
242 | || (r=curl_easy_setopt(curl,CURLOPT_POSTFIELDS,request.data())) | ||
243 | || (r=curl_easy_setopt(curl,CURLOPT_POSTFIELDSIZE,request.length())) | ||
244 | || (r=curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,_curl_tostring)) | ||
245 | || (r=curl_easy_setopt(curl,CURLOPT_WRITEDATA,&response)) | ||
246 | ; | ||
247 | if(r) | ||
248 | throw exception_curl(OPKELE_CP_ "failed to curl_easy_setopt()",r); | ||
249 | if(r=curl_easy_perform(curl)) | ||
250 | throw exception_curl(OPKELE_CP_ "failed to curl_easy_perform()",r); | ||
251 | params_t pp; pp.parse_keyvalues(response); | ||
252 | if(pp.has_param("invalidate_handle")) | ||
253 | invalidate_assoc(server,pp.get_param("invalidate_handle")); | ||
254 | if(pp.has_param("is_valid")) { | ||
255 | if(pp.get_param("is_valid")=="true") | ||
256 | return; | ||
257 | }else if(pp.has_param("lifetime")) { | ||
258 | if(util::string_to_long(pp.get_param("lifetime"))) | ||
259 | return; | ||
260 | } | ||
261 | throw failed_check_authentication(OPKELE_CP_ "failed to verify response"); | ||
262 | } | ||
263 | |||
264 | void consumer_t::retrieve_links(const string& url,string& server,string& delegate) { | ||
265 | server.erase(); | ||
266 | delegate.erase(); | ||
267 | curl_t curl = curl_easy_init(); | ||
268 | if(!curl) | ||
269 | throw exception_curl(OPKELE_CP_ "failed to curl_easy_init()"); | ||
270 | string html; | ||
271 | CURLcode r; | ||
272 | (r=curl_misc_sets(curl)) | ||
273 | || (r=curl_easy_setopt(curl,CURLOPT_URL,url.c_str())) | ||
274 | || (r=curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,_curl_tostring)) | ||
275 | || (r=curl_easy_setopt(curl,CURLOPT_WRITEDATA,&html)) | ||
276 | ; | ||
277 | if(r) | ||
278 | throw exception_curl(OPKELE_CP_ "failed to curl_easy_setopt()",r); | ||
279 | r = curl_easy_perform(curl); | ||
280 | if(r && r!=CURLE_WRITE_ERROR) | ||
281 | throw exception_curl(OPKELE_CP_ "failed to curl_easy_perform()",r); | ||
282 | pcrepp::Pcre bre("<body\\b",PCRE_CASELESS); | ||
283 | // strip out everything past body | ||
284 | if(bre.search(html)) | ||
285 | html.erase(bre.get_match_start()); | ||
286 | pcrepp::Pcre hdre("<head[^>]*>",PCRE_CASELESS); | ||
287 | if(!hdre.search(html)) | ||
288 | throw bad_input(OPKELE_CP_ "failed to find head"); | ||
289 | html.erase(0,hdre.get_match_end()+1); | ||
290 | pcrepp::Pcre lre("<link\\b([^>]+)>",PCRE_CASELESS), | ||
291 | rre("\\brel=['\"]([^'\"]+)['\"]",PCRE_CASELESS), | ||
292 | hre("\\bhref=['\"]([^'\"]+)['\"]",PCRE_CASELESS); | ||
293 | while(lre.search(html)) { | ||
294 | string attrs = lre[0]; | ||
295 | html.erase(0,lre.get_match_end()+1); | ||
296 | if(!(rre.search(attrs)&&hre.search(attrs))) | ||
297 | continue; | ||
298 | if(rre[0]=="openid.server") { | ||
299 | server = hre[0]; | ||
300 | if(!delegate.empty()) | ||
301 | break; | ||
302 | }else if(rre[0]=="openid.delegate") { | ||
303 | delegate = hre[0]; | ||
304 | if(!server.empty()) | ||
305 | break; | ||
306 | } | ||
307 | } | ||
308 | if(server.empty()) | ||
309 | throw failed_assertion(OPKELE_CP_ "The location has no openid.server declaration"); | ||
310 | } | ||
311 | |||
312 | assoc_t consumer_t::find_assoc(const string& server) { | ||
313 | throw failed_lookup(OPKELE_CP_ "no find_assoc() provided"); | ||
314 | } | ||
315 | |||
316 | } | ||