-rw-r--r-- | lib/basic_rp.cc | 311 |
1 files changed, 311 insertions, 0 deletions
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 @@ | |||
1 | #include <openssl/sha.h> | ||
2 | #include <openssl/hmac.h> | ||
3 | #include <opkele/basic_rp.h> | ||
4 | #include <opkele/exception.h> | ||
5 | #include <opkele/uris.h> | ||
6 | #include <opkele/data.h> | ||
7 | #include <opkele/util.h> | ||
8 | #include <opkele/curl.h> | ||
9 | |||
10 | namespace opkele { | ||
11 | |||
12 | static void dh_get_secret( | ||
13 | secret_t& secret, const basic_openid_message& om, | ||
14 | const char *exp_assoc, const char *exp_sess, | ||
15 | util::dh_t& dh, | ||
16 | size_t d_len, unsigned char *(*d_fun)(const unsigned char*,size_t,unsigned char*) ) try { | ||
17 | if(om.get_field("assoc_type")!=exp_assoc || om.get_field("session_type")!=exp_sess) | ||
18 | throw bad_input(OPKELE_CP_ "Unexpected associate response"); | ||
19 | util::bignum_t s_pub = util::base64_to_bignum(om.get_field("dh_server_public")); | ||
20 | vector<unsigned char> ck(DH_size(dh)+1); | ||
21 | unsigned char *ckptr = &(ck.front())+1; | ||
22 | int cklen = DH_compute_key(ckptr,s_pub,dh); | ||
23 | if(cklen<0) | ||
24 | throw exception_openssl(OPKELE_CP_ "failed to DH_compute_key()"); | ||
25 | if(cklen && (*ckptr)&0x80) { | ||
26 | (*(--ckptr))=0; ++cklen; } | ||
27 | unsigned char key_digest[d_len]; | ||
28 | secret.enxor_from_base64((*d_fun)(ckptr,cklen,key_digest),om.get_field("enc_mac_key")); | ||
29 | }catch(opkele::failed_lookup& ofl) { | ||
30 | throw bad_input(OPKELE_CP_ "Incoherent response from OP"); | ||
31 | } OPKELE_RETHROW | ||
32 | |||
33 | static void direct_request(basic_openid_message& oum,const basic_openid_message& inm,const string& OP) { | ||
34 | util::curl_pick_t curl = util::curl_pick_t::easy_init(); | ||
35 | if(!curl) | ||
36 | throw exception_curl(OPKELE_CP_ "failed to initialize curl"); | ||
37 | string request = inm.query_string(); | ||
38 | CURLcode r; | ||
39 | (r=curl.misc_sets()) | ||
40 | || (r=curl.easy_setopt(CURLOPT_URL,OP.c_str())) | ||
41 | || (r=curl.easy_setopt(CURLOPT_POST,1)) | ||
42 | || (r=curl.easy_setopt(CURLOPT_POSTFIELDS,request.data())) | ||
43 | || (r=curl.easy_setopt(CURLOPT_POSTFIELDSIZE,request.length())) | ||
44 | || (r=curl.set_write()); | ||
45 | if(r) | ||
46 | throw exception_curl(OPKELE_CP_ "failed to set curly options",r); | ||
47 | if( (r=curl.easy_perform()) ) | ||
48 | throw exception_curl(OPKELE_CP_ "failed to perform curly request",r); | ||
49 | oum.from_keyvalues(curl.response); | ||
50 | } | ||
51 | |||
52 | |||
53 | assoc_t basic_RP::associate(const string& OP) { | ||
54 | util::dh_t dh = DH_new(); | ||
55 | if(!dh) | ||
56 | throw exception_openssl(OPKELE_CP_ "failed to DH_new()"); | ||
57 | dh->p = util::dec_to_bignum(data::_default_p); | ||
58 | dh->g = util::dec_to_bignum(data::_default_g); | ||
59 | if(!DH_generate_key(dh)) | ||
60 | throw exception_openssl(OPKELE_CP_ "failed to DH_generate_key()"); | ||
61 | openid_message_t req; | ||
62 | req.set_field("ns",OIURI_OPENID20); | ||
63 | req.set_field("mode","associate"); | ||
64 | req.set_field("dh_modulus",util::bignum_to_base64(dh->p)); | ||
65 | req.set_field("dh_gen",util::bignum_to_base64(dh->g)); | ||
66 | req.set_field("dh_consumer_public",util::bignum_to_base64(dh->pub_key)); | ||
67 | openid_message_t res; | ||
68 | req.set_field("assoc_type","HMAC-SHA256"); | ||
69 | req.set_field("session_type","DH-SHA256"); | ||
70 | secret_t secret; | ||
71 | int expires_in; | ||
72 | try { | ||
73 | direct_request(res,req,OP); | ||
74 | dh_get_secret( secret, res, | ||
75 | "HMAC-SHA256", "DH-SHA256", | ||
76 | dh, SHA256_DIGEST_LENGTH, SHA256 ); | ||
77 | expires_in = util::string_to_long(res.get_field("expires_in")); | ||
78 | }catch(exception& e) { | ||
79 | try { | ||
80 | req.set_field("assoc_type","HMAC-SHA1"); | ||
81 | req.set_field("session_type","DH-SHA1"); | ||
82 | direct_request(res,req,OP); | ||
83 | dh_get_secret( secret, res, | ||
84 | "HMAC-SHA1", "DH-SHA1", | ||
85 | dh, SHA_DIGEST_LENGTH, SHA1 ); | ||
86 | expires_in = util::string_to_long(res.get_field("expires_in")); | ||
87 | }catch(bad_input& e) { | ||
88 | throw dumb_RP(OPKELE_CP_ "OP failed to supply an association"); | ||
89 | } | ||
90 | } | ||
91 | return store_assoc( | ||
92 | OP, res.get_field("assoc_handle"), | ||
93 | res.get_field("assoc_type"), secret, | ||
94 | expires_in ); | ||
95 | } | ||
96 | |||
97 | basic_openid_message& basic_RP::checkid_( | ||
98 | basic_openid_message& rv, | ||
99 | mode_t mode, | ||
100 | const string& return_to,const string& realm, | ||
101 | extension_t *ext) { | ||
102 | rv.reset_fields(); | ||
103 | rv.set_field("ns",OIURI_OPENID20); | ||
104 | if(mode==mode_checkid_immediate) | ||
105 | rv.set_field("mode","checkid_immediate"); | ||
106 | else if(mode==mode_checkid_setup) | ||
107 | rv.set_field("mode","checkid_setup"); | ||
108 | else | ||
109 | throw bad_input(OPKELE_CP_ "unknown checkid_* mode"); | ||
110 | if(realm.empty() && return_to.empty()) | ||
111 | throw bad_input(OPKELE_CP_ "At least one of realm and return_to must be non-empty"); | ||
112 | if(!realm.empty()) { | ||
113 | rv.set_field("realm",realm); | ||
114 | rv.set_field("trust_root",realm); | ||
115 | } | ||
116 | if(!return_to.empty()) | ||
117 | rv.set_field("return_to",return_to); | ||
118 | const openid_endpoint_t& ep = get_endpoint(); | ||
119 | rv.set_field("claimed_id",ep.claimed_id); | ||
120 | rv.set_field("identity",ep.local_id); | ||
121 | try { | ||
122 | rv.set_field("assoc_handle",find_assoc(ep.uri)->handle()); | ||
123 | }catch(dumb_RP& drp) { | ||
124 | }catch(failed_lookup& fl) { | ||
125 | try { | ||
126 | rv.set_field("assoc_handle",associate(ep.uri)->handle()); | ||
127 | }catch(dumb_RP& drp) { } | ||
128 | } OPKELE_RETHROW | ||
129 | if(ext) ext->checkid_hook(rv); | ||
130 | return rv; | ||
131 | } | ||
132 | |||
133 | class signed_part_message_proxy : public basic_openid_message { | ||
134 | public: | ||
135 | const basic_openid_message& x; | ||
136 | set<string> signeds; | ||
137 | |||
138 | signed_part_message_proxy(const basic_openid_message& xx) : x(xx) { | ||
139 | const string& slist = x.get_field("signed"); | ||
140 | string::size_type p = 0; | ||
141 | while(true) { | ||
142 | string::size_type co = slist.find(',',p); | ||
143 | string f = (co==string::npos) | ||
144 | ?slist.substr(p):slist.substr(p,co-p); | ||
145 | signeds.insert(f); | ||
146 | if(co==string::npos) break; | ||
147 | p = co+1; | ||
148 | } | ||
149 | } | ||
150 | |||
151 | bool has_field(const string& n) const { | ||
152 | return signeds.find(n)!=signeds.end() && x.has_field(n); } | ||
153 | const string& get_field(const string& n) const { | ||
154 | if(signeds.find(n)==signeds.end()) | ||
155 | throw failed_lookup(OPKELE_CP_ "The field isn't known to be signed"); | ||
156 | return x.get_field(n); } | ||
157 | |||
158 | fields_iterator fields_begin() const { | ||
159 | return signeds.begin(); } | ||
160 | fields_iterator fields_end() const { | ||
161 | return signeds.end(); } | ||
162 | }; | ||
163 | |||
164 | static void parse_query(const string& u,string::size_type q, | ||
165 | map<string,string>& p) { | ||
166 | if(q==string::npos) | ||
167 | return; | ||
168 | assert(u[q]=='?'); | ||
169 | ++q; | ||
170 | string::size_type l = u.size(); | ||
171 | while(q<l) { | ||
172 | string::size_type eq = u.find('=',q); | ||
173 | string::size_type am = u.find('&',q); | ||
174 | if(am==string::npos) { | ||
175 | if(eq==string::npos) { | ||
176 | p[""] = u.substr(q); | ||
177 | }else{ | ||
178 | p[u.substr(q,eq-q)] = u.substr(eq+1); | ||
179 | } | ||
180 | break; | ||
181 | }else{ | ||
182 | if(eq==string::npos || eq>am) { | ||
183 | p[""] = u.substr(q,eq-q); | ||
184 | }else{ | ||
185 | p[u.substr(q,eq-q)] = u.substr(eq+1,am-eq-1); | ||
186 | } | ||
187 | q = ++am; | ||
188 | } | ||
189 | } | ||
190 | } | ||
191 | |||
192 | void basic_RP::id_res(const basic_openid_message& om,extension_t *ext) { | ||
193 | bool o2 = om.has_field("ns") | ||
194 | && om.get_field("ns")==OIURI_OPENID20; | ||
195 | if( (!o2) && om.has_field("user_setup_url")) | ||
196 | throw id_res_setup(OPKELE_CP_ "assertion failed, setup url provided", | ||
197 | om.get_field("user_setup_url")); | ||
198 | string m = om.get_field("mode"); | ||
199 | if(o2 && m=="setup_needed") | ||
200 | throw id_res_setup(OPKELE_CP_ "setup needed, no setup url provided"); | ||
201 | if(m=="cancel") | ||
202 | throw id_res_cancel(OPKELE_CP_ "authentication cancelled"); | ||
203 | bool go_dumb=false; | ||
204 | try { | ||
205 | string OP = o2 | ||
206 | ?om.get_field("op_endpoint") | ||
207 | :get_endpoint().uri; | ||
208 | assoc_t assoc = retrieve_assoc( | ||
209 | OP,om.get_field("assoc_handle")); | ||
210 | if(om.get_field("sig")!=util::base64_signature(assoc,om)) | ||
211 | throw id_res_mismatch(OPKELE_CP_ "signature mismatch"); | ||
212 | }catch(dumb_RP& drp) { | ||
213 | go_dumb=true; | ||
214 | }catch(failed_lookup& e) { | ||
215 | go_dumb=true; | ||
216 | } OPKELE_RETHROW | ||
217 | if(go_dumb) { | ||
218 | try { | ||
219 | string OP = o2 | ||
220 | ?om.get_field("op_endpoint") | ||
221 | :get_endpoint().uri; | ||
222 | check_authentication(OP,om); | ||
223 | }catch(failed_check_authentication& fca) { | ||
224 | throw id_res_failed(OPKELE_CP_ "failed to check_authentication()"); | ||
225 | } OPKELE_RETHROW | ||
226 | } | ||
227 | signed_part_message_proxy signeds(om); | ||
228 | if(o2) { | ||
229 | check_nonce(om.get_field("op_endpoint"), | ||
230 | om.get_field("response_nonce")); | ||
231 | static const char *mustsign[] = { | ||
232 | "op_endpoint", "return_to", "response_nonce", "assoc_handle", | ||
233 | "claimed_id", "identity" }; | ||
234 | for(int ms=0;ms<(sizeof(mustsign)/sizeof(*mustsign));++ms) { | ||
235 | if(om.has_field(mustsign[ms]) && !signeds.has_field(mustsign[ms])) | ||
236 | throw bad_input(OPKELE_CP_ string("Field '")+mustsign[ms]+"' is not signed against the specs"); | ||
237 | } | ||
238 | if( ( | ||
239 | (om.has_field("claimed_id")?1:0) | ||
240 | ^ | ||
241 | (om.has_field("identity")?1:0) | ||
242 | )&1 ) | ||
243 | throw bad_input(OPKELE_CP_ "claimed_id and identity must be either both present or both absent"); | ||
244 | |||
245 | string turl = util::rfc_3986_normalize_uri(get_this_url()); | ||
246 | util::strip_uri_fragment_part(turl); | ||
247 | string rurl = util::rfc_3986_normalize_uri(om.get_field("return_to")); | ||
248 | util::strip_uri_fragment_part(rurl); | ||
249 | string::size_type | ||
250 | tq = turl.find('?'), rq = rurl.find('?'); | ||
251 | if( | ||
252 | ((tq==string::npos)?turl:turl.substr(0,tq)) | ||
253 | != | ||
254 | ((rq==string::npos)?rurl:rurl.substr(0,rq)) | ||
255 | ) | ||
256 | throw id_res_bad_return_to(OPKELE_CP_ "return_to url doesn't match request url"); | ||
257 | map<string,string> tp; parse_query(turl,tq,tp); | ||
258 | map<string,string> rp; parse_query(rurl,rq,rp); | ||
259 | for(map<string,string>::const_iterator rpi=rp.begin();rpi!=rp.end();++rpi) { | ||
260 | map<string,string>::const_iterator tpi = tp.find(rpi->first); | ||
261 | if(tpi==tp.end()) | ||
262 | throw id_res_bad_return_to(OPKELE_CP_ string("Parameter '")+rpi->first+"' from return_to is missing from the request"); | ||
263 | if(tpi->second!=rpi->second) | ||
264 | throw id_res_bad_return_to(OPKELE_CP_ string("Parameter '")+rpi->first+"' from return_to doesn't matche the request"); | ||
265 | } | ||
266 | |||
267 | if(om.has_field("claimed_id")) { | ||
268 | verify_OP( | ||
269 | om.get_field("op_endpoint"), | ||
270 | om.get_field("claimed_id"), | ||
271 | om.get_field("identity") ); | ||
272 | } | ||
273 | |||
274 | } | ||
275 | if(ext) ext->id_res_hook(om,signeds); | ||
276 | } | ||
277 | |||
278 | class check_auth_message_proxy : public basic_openid_message { | ||
279 | public: | ||
280 | const basic_openid_message& x; | ||
281 | |||
282 | check_auth_message_proxy(const basic_openid_message& xx) : x(xx) { } | ||
283 | |||
284 | bool has_field(const string& n) const { return x.has_field(n); } | ||
285 | const string& get_field(const string& n) const { | ||
286 | static const string checkauthmode="check_authentication"; | ||
287 | return (n=="mode")?checkauthmode:x.get_field(n); } | ||
288 | bool has_ns(const string& uri) const {return x.has_ns(uri); } | ||
289 | string get_ns(const string& uri) const { return x.get_ns(uri); } | ||
290 | fields_iterator fields_begin() const { | ||
291 | return x.fields_begin(); } | ||
292 | fields_iterator fields_end() const { | ||
293 | return x.fields_end(); } | ||
294 | }; | ||
295 | |||
296 | void basic_RP::check_authentication(const string& OP, | ||
297 | const basic_openid_message& om){ | ||
298 | openid_message_t res; | ||
299 | direct_request(res,check_auth_message_proxy(om),OP); | ||
300 | if(res.has_field("is_valid")) { | ||
301 | if(res.get_field("is_valid")=="true") { | ||
302 | if(res.has_field("invalidate_handle")) | ||
303 | invalidate_assoc(OP,res.get_field("invalidate_handle")); | ||
304 | return; | ||
305 | } | ||
306 | } | ||
307 | throw failed_check_authentication( | ||
308 | OPKELE_CP_ "failed to verify response"); | ||
309 | } | ||
310 | |||
311 | } | ||