1 #include "ssl_internal.h" |
|
2 |
|
3 #include <gnutls/x509.h> |
|
4 |
|
5 #include <stdlib.h> |
|
6 #include <string.h> |
|
7 #include <time.h> |
|
8 |
|
9 // XXX: remove |
|
10 #include "log.h" |
|
11 #include <assert.h> |
|
12 |
|
13 |
|
14 /** |
|
15 * Cast a ssl_client to a sock_fd. |
|
16 */ |
|
17 #define SSL_CLIENT_FD(client_ptr) (&(client_ptr)->base_tcp.base_trans.base_fd) |
|
18 |
|
19 /** |
|
20 * Cast a ssl_client to a sock_stream. |
|
21 */ |
|
22 #define SSL_CLIENT_TRANSPORT(client_ptr) (&(client_ptr)->base_tcp.base_trans.base_fd.base) |
|
23 |
|
24 |
|
25 |
|
26 /** |
|
27 * Enable the TCP events based on the session's gnutls_record_get_direction(). |
|
28 */ |
|
29 static err_t ssl_client_ev_enable (struct ssl_client *client, error_t *err) |
|
30 { |
|
31 int ret; |
|
32 short mask; |
|
33 |
|
34 // gnutls_record_get_direction tells us what I/O operation gnutls would have required for the last |
|
35 // operation, so we can use that to determine what events to register |
|
36 switch ((ret = gnutls_record_get_direction(client->session))) { |
|
37 case 0: |
|
38 // read more data |
|
39 mask = TRANSPORT_READ; |
|
40 break; |
|
41 |
|
42 case 1: |
|
43 // write buffer full |
|
44 mask = TRANSPORT_WRITE; |
|
45 break; |
|
46 |
|
47 default: |
|
48 // random error |
|
49 RETURN_SET_ERROR_EXTRA(err, ERR_GNUTLS_RECORD_GET_DIRECTION, ret); |
|
50 } |
|
51 |
|
52 // do the enabling |
|
53 if ((ERROR_CODE(err) = transport_fd_enable(SSL_CLIENT_FD(client), mask))) |
|
54 return ERROR_CODE(err); |
|
55 |
|
56 |
|
57 return SUCCESS; |
|
58 } |
|
59 |
|
60 /** |
|
61 * Translate a set of gnutls_certificate_status_t values to a constant error message |
|
62 */ |
|
63 static const char* ssl_client_verify_error (unsigned int status) |
|
64 { |
|
65 if (status & GNUTLS_CERT_REVOKED) |
|
66 return "certificate was revoked"; |
|
67 |
|
68 else if (status & GNUTLS_CERT_INVALID) { |
|
69 if (status & GNUTLS_CERT_SIGNER_NOT_FOUND) |
|
70 return "certificate signer was not found"; |
|
71 |
|
72 else if (status & GNUTLS_CERT_SIGNER_NOT_CA) |
|
73 return "certificate signer is not a Certificate Authority"; |
|
74 |
|
75 else if (status & GNUTLS_CERT_INSECURE_ALGORITHM) |
|
76 return "certificate signed using an insecure algorithm"; |
|
77 |
|
78 else |
|
79 return "certificate could not be verified"; |
|
80 |
|
81 } else |
|
82 return "unknown error"; |
|
83 |
|
84 } |
|
85 |
|
86 /** |
|
87 * Perform the certificate validation procedure on the peer cert. |
|
88 * |
|
89 * Based on the GnuTLS examples/ex-rfc2818.c |
|
90 */ |
|
91 static err_t ssl_client_verify (struct ssl_client *client, error_t *err) |
|
92 { |
|
93 unsigned int status; |
|
94 const gnutls_datum_t *cert_list; |
|
95 unsigned int cert_list_size; |
|
96 gnutls_x509_crt_t cert = NULL; |
|
97 time_t t, now; |
|
98 |
|
99 // init |
|
100 RESET_ERROR(err); |
|
101 now = time(NULL); |
|
102 |
|
103 // inspect the peer's cert chain using the installed trusted CAs |
|
104 if ((ERROR_EXTRA(err) = gnutls_certificate_verify_peers2(client->session, &status))) |
|
105 JUMP_SET_ERROR(err, ERR_GNUTLS_CERT_VERIFY_PEERS2); |
|
106 |
|
107 // verify errors? |
|
108 if (status) |
|
109 JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, ssl_client_verify_error(status)); |
|
110 |
|
111 // import the main cert |
|
112 assert(gnutls_certificate_type_get(client->session) == GNUTLS_CRT_X509); |
|
113 |
|
114 if ((ERROR_EXTRA(err) = gnutls_x509_crt_init(&cert))) |
|
115 JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, "gnutls_x509_crt_init"); |
|
116 |
|
117 if ((cert_list = gnutls_certificate_get_peers(client->session, &cert_list_size)) == NULL) |
|
118 JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, "gnutls_certificate_get_peers"); |
|
119 |
|
120 if (!cert_list_size) |
|
121 JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, "cert_list_size"); |
|
122 |
|
123 if ((ERROR_EXTRA(err) = gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER))) |
|
124 JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, "gnutls_x509_crt_import"); |
|
125 |
|
126 // check expire/activate... not sure if we need to do this |
|
127 if ((t = gnutls_x509_crt_get_expiration_time(cert)) == ((time_t) -1) || t < now) |
|
128 JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, "gnutls_x509_crt_get_expiration_time"); |
|
129 |
|
130 if ((t = gnutls_x509_crt_get_activation_time(cert)) == ((time_t) -1) || t > now) |
|
131 JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, "gnutls_x509_crt_get_activation_time"); |
|
132 |
|
133 // check hostname |
|
134 if (!gnutls_x509_crt_check_hostname(cert, client->hostname)) |
|
135 JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, "gnutls_x509_crt_check_hostname"); |
|
136 |
|
137 error: |
|
138 // cleanup |
|
139 if (cert) |
|
140 gnutls_x509_crt_deinit(cert); |
|
141 |
|
142 // should be SUCCESS |
|
143 return ERROR_CODE(err); |
|
144 } |
|
145 |
|
146 |
|
147 /** |
|
148 * Our handshake driver. This will execute the next gnutls_handshake step, handling E_AGAIN. |
|
149 * |
|
150 * This updates the ssl_client::handshake state internally, as used by ssl_client_event_handler. |
|
151 * |
|
152 * If the client is marked as verify, this will perform the verification, returning on any errors, and then unset the |
|
153 * verify flag - this ensures that the peer cert is only verified once per connection... |
|
154 * |
|
155 * @return >0 for finished handshake, 0 for handshake-in-progress, -err_t for errors. |
|
156 */ |
|
157 static int ssl_client_handshake (struct ssl_client *client, error_t *err) |
|
158 { |
|
159 int ret; |
|
160 |
|
161 // perform the handshake |
|
162 if ((ret = gnutls_handshake(client->session)) < 0 && ret != GNUTLS_E_AGAIN) |
|
163 JUMP_SET_ERROR_EXTRA(err, ERR_GNUTLS_HANDSHAKE, ret); |
|
164 |
|
165 // complete? |
|
166 if (ret == 0) { |
|
167 // update state |
|
168 client->handshake = false; |
|
169 |
|
170 // verify? |
|
171 if (client->verify) { |
|
172 // perform the validation |
|
173 if (ssl_client_verify(client, err)) |
|
174 goto error; |
|
175 |
|
176 // unmark |
|
177 client->verify = false; |
|
178 } |
|
179 |
|
180 // handshake done |
|
181 return 1; |
|
182 |
|
183 } else { |
|
184 // set state, isn't really needed every time, but easier this way |
|
185 client->handshake = true; |
|
186 |
|
187 // re-enable the event for the next iteration |
|
188 return ssl_client_ev_enable(client, err); |
|
189 } |
|
190 |
|
191 error: |
|
192 return -ERROR_CODE(err); |
|
193 } |
|
194 |
|
195 /** |
|
196 * Our transport_fd event handler. Drive the handshake if that's current, otherwise, invoke user callbacks. |
|
197 */ |
|
198 static void ssl_client_on_event (struct transport_fd *fd, short what, void *arg) |
|
199 { |
|
200 struct ssl_client *client = arg; |
|
201 error_t err; |
|
202 |
|
203 (void) fd; |
|
204 |
|
205 // XXX: timeouts |
|
206 (void) what; |
|
207 |
|
208 // are we in the handshake cycle? |
|
209 if (client->handshake) { |
|
210 RESET_ERROR(&err); |
|
211 |
|
212 // perform the next handshake step |
|
213 // this returns zero when the handshake is not yet done, errors/completion then trigger the else-if-else below |
|
214 if (ssl_client_handshake(client, &err) == 0) { |
|
215 // handshake continues |
|
216 |
|
217 } else if (!SSL_CLIENT_TRANSPORT(client)->connected) { |
|
218 // the async connect+handshake process has completed |
|
219 // invoke the user connect callback directly with appropriate error |
|
220 transport_connected(SSL_CLIENT_TRANSPORT(client), ERROR_CODE(&err) ? &err : NULL, true); |
|
221 |
|
222 } else { |
|
223 // in-connection re-handshake completed |
|
224 if (ERROR_CODE(&err)) |
|
225 // the re-handshake failed, so this transport is dead |
|
226 transport_error(SSL_CLIENT_TRANSPORT(client), &err); |
|
227 |
|
228 else |
|
229 // re-handshake completed, so continue with the transport_callbacks |
|
230 transport_invoke(SSL_CLIENT_TRANSPORT(client), what); |
|
231 } |
|
232 |
|
233 } else { |
|
234 // normal transport operation |
|
235 // gnutls might be able to proceed now, so invoke user callbacks |
|
236 transport_invoke(SSL_CLIENT_TRANSPORT(client), what); |
|
237 } |
|
238 } |
|
239 |
|
240 static err_t ssl_client__read (transport_t *transport, void *buf, size_t *len, error_t *err) |
|
241 { |
|
242 struct ssl_client *client = transport_check(transport, &ssl_client_type); |
|
243 int ret; |
|
244 |
|
245 // read gnutls record |
|
246 do { |
|
247 ret = gnutls_record_recv(client->session, buf, *len); |
|
248 |
|
249 } while (ret == GNUTLS_E_INTERRUPTED); |
|
250 |
|
251 // errors |
|
252 // XXX: E_REHANDSHAKE? |
|
253 if (ret < 0 && ret != GNUTLS_E_AGAIN) |
|
254 RETURN_SET_ERROR_EXTRA(err, ERR_GNUTLS_RECORD_RECV, ret); |
|
255 |
|
256 else if (ret == 0) |
|
257 return SET_ERROR(err, ERR_EOF); |
|
258 |
|
259 |
|
260 // EAGAIN? |
|
261 if (ret < 0) { |
|
262 *len = 0; |
|
263 |
|
264 } else { |
|
265 // updated length |
|
266 *len = ret; |
|
267 |
|
268 } |
|
269 |
|
270 return SUCCESS; |
|
271 } |
|
272 |
|
273 static err_t ssl_client__write (transport_t *transport, const void *buf, size_t *len, error_t *err) |
|
274 { |
|
275 struct ssl_client *client = transport_check(transport, &ssl_client_type); |
|
276 int ret; |
|
277 |
|
278 // read gnutls record |
|
279 do { |
|
280 ret = gnutls_record_send(client->session, buf, *len); |
|
281 |
|
282 } while (ret == GNUTLS_E_INTERRUPTED); |
|
283 |
|
284 // errors |
|
285 if (ret < 0 && ret != GNUTLS_E_AGAIN) |
|
286 RETURN_SET_ERROR_EXTRA(err, ERR_GNUTLS_RECORD_SEND, ret); |
|
287 |
|
288 else if (ret == 0) |
|
289 return SET_ERROR(err, ERR_WRITE_EOF); |
|
290 |
|
291 |
|
292 // eagain? |
|
293 if (ret < 0) { |
|
294 *len = 0; |
|
295 |
|
296 } else { |
|
297 // updated length |
|
298 *len = ret; |
|
299 } |
|
300 |
|
301 return SUCCESS; |
|
302 } |
|
303 |
|
304 void ssl_client_deinit (struct ssl_client *client) |
|
305 { |
|
306 // close the session rudely |
|
307 gnutls_deinit(client->session); |
|
308 client->session = NULL; |
|
309 |
|
310 // terminate the TCP transport |
|
311 tcp_client_deinit(&client->base_tcp); |
|
312 |
|
313 if (client->cred) { |
|
314 // drop the cred ref |
|
315 ssl_client_cred_put(client->cred); |
|
316 |
|
317 client->cred = NULL; |
|
318 } |
|
319 |
|
320 // free |
|
321 free(client->hostname); |
|
322 client->hostname = NULL; |
|
323 } |
|
324 |
|
325 |
|
326 static void ssl_client__deinit (transport_t *transport) |
|
327 { |
|
328 struct ssl_client *client = transport_check(transport, &ssl_client_type); |
|
329 |
|
330 // die |
|
331 ssl_client_deinit(client); |
|
332 } |
|
333 |
|
334 /** |
|
335 * Our tcp_client-invoked connect handler |
|
336 */ |
|
337 static void ssl_client__connected (transport_t *transport, const error_t *tcp_err) |
|
338 { |
|
339 struct ssl_client *client = transport_check(transport, &ssl_client_type); |
|
340 error_t err; |
|
341 |
|
342 // trap errors to let the user handle them directly |
|
343 if (tcp_err) |
|
344 JUMP_SET_ERROR_INFO(&err, tcp_err); |
|
345 |
|
346 // bind default transport functions (recv/send) to use the TCP fd |
|
347 gnutls_transport_set_ptr(client->session, (gnutls_transport_ptr_t) (long int) SSL_CLIENT_FD(client)->fd); |
|
348 |
|
349 // add ourselves as the event handler |
|
350 if ((ERROR_CODE(&err) = transport_fd_setup(SSL_CLIENT_FD(client), ssl_client_on_event, client))) |
|
351 goto error; |
|
352 |
|
353 // start handshake |
|
354 if (ssl_client_handshake(client, &err)) |
|
355 // this should complete with SUCCESS if it returns >0 |
|
356 goto error; |
|
357 |
|
358 // ok, so we wait... |
|
359 return; |
|
360 |
|
361 error: |
|
362 // tell the user |
|
363 transport_connected(transport, &err, true); |
|
364 } |
|
365 |
|
366 struct transport_type ssl_client_type = { |
|
367 .base_type = { |
|
368 .parent = &tcp_client_type.base_type, |
|
369 }, |
|
370 .methods = { |
|
371 .read = ssl_client__read, |
|
372 .write = ssl_client__write, |
|
373 .deinit = ssl_client__deinit, |
|
374 ._connected = ssl_client__connected, |
|
375 }, |
|
376 }; |
|
377 |
|
378 |
|
379 |
|
380 static void ssl_client_destroy (struct ssl_client *client) |
|
381 { |
|
382 ssl_client_deinit(client); |
|
383 |
|
384 free(client); |
|
385 } |
|
386 |
|
387 err_t ssl_connect (const struct transport_info *info, transport_t **transport_ptr, |
|
388 const char *hostname, const char *service, |
|
389 struct ssl_client_cred *cred, |
|
390 error_t *err |
|
391 ) |
|
392 { |
|
393 struct ssl_client *client = NULL; |
|
394 |
|
395 // alloc |
|
396 if ((client = calloc(1, sizeof(*client))) == NULL) |
|
397 return SET_ERROR(err, ERR_CALLOC); |
|
398 |
|
399 // initialize base |
|
400 transport_init(SSL_CLIENT_TRANSPORT(client), &ssl_client_type, info); |
|
401 |
|
402 if (!cred) { |
|
403 // default credentials |
|
404 cred = &ssl_client_cred_anon; |
|
405 |
|
406 } else { |
|
407 // take a ref |
|
408 client->cred = cred; |
|
409 cred->refcount++; |
|
410 }; |
|
411 |
|
412 // do verify? |
|
413 if (cred->verify) |
|
414 client->verify = true; |
|
415 |
|
416 // init |
|
417 if ((client->hostname = strdup(hostname)) == NULL) |
|
418 JUMP_SET_ERROR(err, ERR_STRDUP); |
|
419 |
|
420 // initialize TCP |
|
421 tcp_client_init(&client->base_tcp); |
|
422 |
|
423 // initialize client session |
|
424 if ((ERROR_EXTRA(err) = gnutls_init(&client->session, GNUTLS_CLIENT)) < 0) |
|
425 JUMP_SET_ERROR(err, ERR_GNUTLS_INIT); |
|
426 |
|
427 // ...default priority stuff |
|
428 if ((ERROR_EXTRA(err) = gnutls_set_default_priority(client->session))) |
|
429 JUMP_SET_ERROR(err, ERR_GNUTLS_SET_DEFAULT_PRIORITY); |
|
430 |
|
431 // XXX: silly hack for OpenSSL interop |
|
432 gnutls_dh_set_prime_bits(client->session, 512); |
|
433 |
|
434 // bind credentials |
|
435 if ((ERROR_EXTRA(err) = gnutls_credentials_set(client->session, GNUTLS_CRD_CERTIFICATE, cred->x509))) |
|
436 JUMP_SET_ERROR(err, ERR_GNUTLS_CRED_SET); |
|
437 |
|
438 // TCP connect |
|
439 if (tcp_client_connect_async(&client->base_tcp, hostname, service, err)) |
|
440 goto error; |
|
441 |
|
442 // done, wait for the connect to complete |
|
443 *transport_ptr = SSL_CLIENT_TRANSPORT(client); |
|
444 |
|
445 return SUCCESS; |
|
446 |
|
447 error: |
|
448 // cleanup |
|
449 ssl_client_destroy(client); |
|
450 |
|
451 return ERROR_CODE(err); |
|
452 } |
|
453 |
|
454 |
|