terom@180: #include "ssl_internal.h" terom@139: terom@140: #include terom@140: terom@2: #include terom@140: #include terom@140: #include terom@140: terom@180: // XXX: remove terom@180: #include "log.h" terom@140: #include terom@2: terom@180: terom@180: /** terom@180: * Cast a ssl_client to a sock_fd. terom@180: */ terom@180: #define SSL_CLIENT_FD(client_ptr) (&(client_ptr)->base_tcp.base_trans.base_fd) terom@180: terom@180: /** terom@180: * Cast a ssl_client to a sock_stream. terom@180: */ terom@180: #define SSL_CLIENT_TRANSPORT(client_ptr) (&(client_ptr)->base_tcp.base_trans.base_fd.base) terom@180: terom@180: terom@180: terom@139: /** terom@155: * Enable the TCP events based on the session's gnutls_record_get_direction(). terom@139: */ terom@180: static err_t ssl_client_ev_enable (struct ssl_client *client, error_t *err) terom@139: { terom@139: int ret; terom@155: short mask; terom@139: terom@139: // gnutls_record_get_direction tells us what I/O operation gnutls would have required for the last terom@139: // operation, so we can use that to determine what events to register terom@180: switch ((ret = gnutls_record_get_direction(client->session))) { terom@139: case 0: terom@139: // read more data terom@156: mask = TRANSPORT_READ; terom@139: break; terom@139: terom@139: case 1: terom@139: // write buffer full terom@156: mask = TRANSPORT_WRITE; terom@139: break; terom@139: terom@139: default: terom@139: // random error terom@139: RETURN_SET_ERROR_EXTRA(err, ERR_GNUTLS_RECORD_GET_DIRECTION, ret); terom@139: } terom@139: terom@155: // do the enabling terom@180: if ((ERROR_CODE(err) = transport_fd_enable(SSL_CLIENT_FD(client), mask))) terom@155: return ERROR_CODE(err); terom@155: terom@155: terom@139: return SUCCESS; terom@139: } terom@139: terom@139: /** terom@140: * Translate a set of gnutls_certificate_status_t values to a constant error message terom@140: */ terom@180: static const char* ssl_client_verify_error (unsigned int status) terom@140: { terom@140: if (status & GNUTLS_CERT_REVOKED) terom@140: return "certificate was revoked"; terom@140: terom@140: else if (status & GNUTLS_CERT_INVALID) { terom@140: if (status & GNUTLS_CERT_SIGNER_NOT_FOUND) terom@140: return "certificate signer was not found"; terom@140: terom@140: else if (status & GNUTLS_CERT_SIGNER_NOT_CA) terom@140: return "certificate signer is not a Certificate Authority"; terom@140: terom@140: else if (status & GNUTLS_CERT_INSECURE_ALGORITHM) terom@140: return "certificate signed using an insecure algorithm"; terom@140: terom@140: else terom@140: return "certificate could not be verified"; terom@140: terom@140: } else terom@140: return "unknown error"; terom@140: terom@140: } terom@140: terom@140: /** terom@180: * Perform the certificate validation procedure on the peer cert. terom@140: * terom@140: * Based on the GnuTLS examples/ex-rfc2818.c terom@140: */ terom@180: static err_t ssl_client_verify (struct ssl_client *client, error_t *err) terom@140: { terom@140: unsigned int status; terom@140: const gnutls_datum_t *cert_list; terom@140: unsigned int cert_list_size; terom@140: gnutls_x509_crt_t cert = NULL; terom@140: time_t t, now; terom@140: terom@140: // init terom@140: RESET_ERROR(err); terom@140: now = time(NULL); terom@140: terom@140: // inspect the peer's cert chain using the installed trusted CAs terom@180: if ((ERROR_EXTRA(err) = gnutls_certificate_verify_peers2(client->session, &status))) terom@140: JUMP_SET_ERROR(err, ERR_GNUTLS_CERT_VERIFY_PEERS2); terom@140: terom@140: // verify errors? terom@140: if (status) terom@180: JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, ssl_client_verify_error(status)); terom@140: terom@140: // import the main cert terom@180: assert(gnutls_certificate_type_get(client->session) == GNUTLS_CRT_X509); terom@140: terom@140: if ((ERROR_EXTRA(err) = gnutls_x509_crt_init(&cert))) terom@140: JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, "gnutls_x509_crt_init"); terom@140: terom@180: if ((cert_list = gnutls_certificate_get_peers(client->session, &cert_list_size)) == NULL) terom@140: JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, "gnutls_certificate_get_peers"); terom@140: terom@140: if (!cert_list_size) terom@140: JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, "cert_list_size"); terom@140: terom@140: if ((ERROR_EXTRA(err) = gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER))) terom@140: JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, "gnutls_x509_crt_import"); terom@140: terom@140: // check expire/activate... not sure if we need to do this terom@140: if ((t = gnutls_x509_crt_get_expiration_time(cert)) == ((time_t) -1) || t < now) terom@140: JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, "gnutls_x509_crt_get_expiration_time"); terom@140: terom@140: if ((t = gnutls_x509_crt_get_activation_time(cert)) == ((time_t) -1) || t > now) terom@140: JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, "gnutls_x509_crt_get_activation_time"); terom@140: terom@140: // check hostname terom@180: if (!gnutls_x509_crt_check_hostname(cert, client->hostname)) terom@140: JUMP_SET_ERROR_STR(err, ERR_GNUTLS_CERT_VERIFY, "gnutls_x509_crt_check_hostname"); terom@140: terom@140: error: terom@140: // cleanup terom@140: if (cert) terom@140: gnutls_x509_crt_deinit(cert); terom@140: terom@140: // should be SUCCESS terom@140: return ERROR_CODE(err); terom@140: } terom@140: terom@140: terom@140: /** terom@139: * Our handshake driver. This will execute the next gnutls_handshake step, handling E_AGAIN. terom@139: * terom@180: * This updates the ssl_client::handshake state internally, as used by ssl_client_event_handler. terom@139: * terom@180: * If the client is marked as verify, this will perform the verification, returning on any errors, and then unset the terom@140: * verify flag - this ensures that the peer cert is only verified once per connection... terom@140: * terom@139: * @return >0 for finished handshake, 0 for handshake-in-progress, -err_t for errors. terom@139: */ terom@180: static int ssl_client_handshake (struct ssl_client *client, error_t *err) terom@139: { terom@139: int ret; terom@139: terom@139: // perform the handshake terom@180: if ((ret = gnutls_handshake(client->session)) < 0 && ret != GNUTLS_E_AGAIN) terom@139: JUMP_SET_ERROR_EXTRA(err, ERR_GNUTLS_HANDSHAKE, ret); terom@139: terom@139: // complete? terom@139: if (ret == 0) { terom@139: // update state terom@180: client->handshake = false; terom@139: terom@140: // verify? terom@180: if (client->verify) { terom@140: // perform the validation terom@180: if (ssl_client_verify(client, err)) terom@140: goto error; terom@140: terom@140: // unmark terom@180: client->verify = false; terom@140: } terom@140: terom@139: // handshake done terom@139: return 1; terom@139: terom@139: } else { terom@139: // set state, isn't really needed every time, but easier this way terom@180: client->handshake = true; terom@139: terom@139: // re-enable the event for the next iteration terom@180: return ssl_client_ev_enable(client, err); terom@139: } terom@139: terom@139: error: terom@139: return -ERROR_CODE(err); terom@139: } terom@139: terom@139: /** terom@155: * Our transport_fd event handler. Drive the handshake if that's current, otherwise, invoke user callbacks. terom@139: */ terom@180: static void ssl_client_on_event (struct transport_fd *fd, short what, void *arg) terom@139: { terom@180: struct ssl_client *client = arg; terom@155: error_t err; terom@139: terom@139: (void) fd; terom@155: terom@155: // XXX: timeouts terom@139: (void) what; terom@139: terom@139: // are we in the handshake cycle? terom@180: if (client->handshake) { terom@139: RESET_ERROR(&err); terom@139: terom@155: // perform the next handshake step terom@183: // this returns zero when the handshake is not yet done, errors/completion then trigger the else-if-else below terom@180: if (ssl_client_handshake(client, &err) == 0) { terom@155: // handshake continues terom@139: terom@183: } else if (!SSL_CLIENT_TRANSPORT(client)->connected) { terom@183: // the async connect+handshake process has completed terom@140: // invoke the user connect callback directly with appropriate error terom@180: transport_connected(SSL_CLIENT_TRANSPORT(client), ERROR_CODE(&err) ? &err : NULL, true); terom@139: terom@139: } else { terom@183: // in-connection re-handshake completed terom@139: if (ERROR_CODE(&err)) terom@155: // the re-handshake failed, so this transport is dead terom@180: transport_error(SSL_CLIENT_TRANSPORT(client), &err); terom@155: terom@155: else terom@155: // re-handshake completed, so continue with the transport_callbacks terom@180: transport_invoke(SSL_CLIENT_TRANSPORT(client), what); terom@139: } terom@139: terom@139: } else { terom@180: // normal transport operation terom@155: // gnutls might be able to proceed now, so invoke user callbacks terom@180: transport_invoke(SSL_CLIENT_TRANSPORT(client), what); terom@139: } terom@139: } terom@139: terom@180: static err_t ssl_client__read (transport_t *transport, void *buf, size_t *len, error_t *err) terom@10: { terom@180: struct ssl_client *client = transport_check(transport, &ssl_client_type); terom@10: int ret; terom@10: terom@12: // read gnutls record terom@155: do { terom@180: ret = gnutls_record_recv(client->session, buf, *len); terom@155: terom@155: } while (ret == GNUTLS_E_INTERRUPTED); terom@10: terom@12: // errors terom@155: // XXX: E_REHANDSHAKE? terom@12: if (ret < 0 && ret != GNUTLS_E_AGAIN) terom@12: RETURN_SET_ERROR_EXTRA(err, ERR_GNUTLS_RECORD_RECV, ret); terom@12: terom@12: else if (ret == 0) terom@163: return SET_ERROR(err, ERR_EOF); terom@12: terom@12: terom@155: // EAGAIN? terom@14: if (ret < 0) { terom@12: *len = 0; terom@12: terom@12: } else { terom@12: // updated length terom@12: *len = ret; terom@12: terom@12: } terom@10: terom@10: return SUCCESS; terom@10: } terom@10: terom@180: static err_t ssl_client__write (transport_t *transport, const void *buf, size_t *len, error_t *err) terom@10: { terom@180: struct ssl_client *client = transport_check(transport, &ssl_client_type); terom@10: int ret; terom@12: terom@12: // read gnutls record terom@155: do { terom@180: ret = gnutls_record_send(client->session, buf, *len); terom@155: terom@155: } while (ret == GNUTLS_E_INTERRUPTED); terom@155: terom@12: // errors terom@12: if (ret < 0 && ret != GNUTLS_E_AGAIN) terom@163: RETURN_SET_ERROR_EXTRA(err, ERR_GNUTLS_RECORD_SEND, ret); terom@10: terom@12: else if (ret == 0) terom@163: return SET_ERROR(err, ERR_WRITE_EOF); terom@12: terom@12: terom@12: // eagain? terom@14: if (ret < 0) { terom@12: *len = 0; terom@12: terom@12: } else { terom@12: // updated length terom@12: *len = ret; terom@12: } terom@10: terom@10: return SUCCESS; terom@10: } terom@10: terom@180: void ssl_client_deinit (struct ssl_client *client) terom@2: { terom@180: // close the session rudely terom@180: gnutls_deinit(client->session); terom@180: client->session = NULL; terom@180: terom@180: // terminate the TCP transport terom@180: tcp_client_deinit(&client->base_tcp); terom@180: terom@180: if (client->cred) { terom@180: // drop the cred ref terom@180: ssl_client_cred_put(client->cred); terom@180: terom@180: client->cred = NULL; terom@180: } terom@180: terom@180: // free terom@180: free(client->hostname); terom@180: client->hostname = NULL; terom@180: } terom@180: terom@180: terom@180: static void ssl_client__deinit (transport_t *transport) terom@180: { terom@180: struct ssl_client *client = transport_check(transport, &ssl_client_type); terom@12: terom@155: // die terom@180: ssl_client_deinit(client); terom@28: } terom@28: terom@139: /** terom@180: * Our tcp_client-invoked connect handler terom@139: */ terom@180: static void ssl_client__connected (transport_t *transport, const error_t *tcp_err) terom@139: { terom@180: struct ssl_client *client = transport_check(transport, &ssl_client_type); terom@155: error_t err; terom@139: terom@139: // trap errors to let the user handle them directly terom@139: if (tcp_err) terom@139: JUMP_SET_ERROR_INFO(&err, tcp_err); terom@139: terom@139: // bind default transport functions (recv/send) to use the TCP fd terom@180: gnutls_transport_set_ptr(client->session, (gnutls_transport_ptr_t) (long int) SSL_CLIENT_FD(client)->fd); terom@139: terom@139: // add ourselves as the event handler terom@180: if ((ERROR_CODE(&err) = transport_fd_setup(SSL_CLIENT_FD(client), ssl_client_on_event, client))) terom@139: goto error; terom@139: terom@139: // start handshake terom@180: if (ssl_client_handshake(client, &err)) terom@139: // this should complete with SUCCESS if it returns >0 terom@139: goto error; terom@139: terom@139: // ok, so we wait... terom@139: return; terom@139: terom@139: error: terom@139: // tell the user terom@155: transport_connected(transport, &err, true); terom@139: } terom@139: terom@180: struct transport_type ssl_client_type = { terom@180: .base_type = { terom@180: .parent = &tcp_client_type.base_type, terom@180: }, terom@180: .methods = { terom@180: .read = ssl_client__read, terom@180: .write = ssl_client__write, terom@180: .deinit = ssl_client__deinit, terom@180: ._connected = ssl_client__connected, terom@27: }, terom@2: }; terom@2: terom@2: terom@14: terom@180: static void ssl_client_destroy (struct ssl_client *client) terom@180: { terom@180: ssl_client_deinit(client); terom@2: terom@180: free(client); terom@140: } terom@140: terom@180: err_t ssl_connect (const struct transport_info *info, transport_t **transport_ptr, terom@140: const char *hostname, const char *service, terom@180: struct ssl_client_cred *cred, terom@155: error_t *err terom@140: ) terom@2: { terom@180: struct ssl_client *client = NULL; terom@2: terom@2: // alloc terom@180: if ((client = calloc(1, sizeof(*client))) == NULL) terom@5: return SET_ERROR(err, ERR_CALLOC); terom@2: terom@5: // initialize base terom@180: transport_init(SSL_CLIENT_TRANSPORT(client), &ssl_client_type, info); terom@2: terom@140: if (!cred) { terom@140: // default credentials terom@180: cred = &ssl_client_cred_anon; terom@140: terom@140: } else { terom@140: // take a ref terom@180: client->cred = cred; terom@140: cred->refcount++; terom@140: }; terom@140: terom@140: // do verify? terom@140: if (cred->verify) terom@180: client->verify = true; terom@140: terom@140: // init terom@180: if ((client->hostname = strdup(hostname)) == NULL) terom@140: JUMP_SET_ERROR(err, ERR_STRDUP); terom@140: terom@155: // initialize TCP terom@180: tcp_client_init(&client->base_tcp); terom@155: terom@2: // initialize client session terom@180: if ((ERROR_EXTRA(err) = gnutls_init(&client->session, GNUTLS_CLIENT)) < 0) terom@5: JUMP_SET_ERROR(err, ERR_GNUTLS_INIT); terom@2: terom@2: // ...default priority stuff terom@180: if ((ERROR_EXTRA(err) = gnutls_set_default_priority(client->session))) terom@5: JUMP_SET_ERROR(err, ERR_GNUTLS_SET_DEFAULT_PRIORITY); terom@2: terom@140: // XXX: silly hack for OpenSSL interop terom@180: gnutls_dh_set_prime_bits(client->session, 512); terom@140: terom@140: // bind credentials terom@180: if ((ERROR_EXTRA(err) = gnutls_credentials_set(client->session, GNUTLS_CRD_CERTIFICATE, cred->x509))) terom@5: JUMP_SET_ERROR(err, ERR_GNUTLS_CRED_SET); terom@2: terom@2: // TCP connect terom@180: if (tcp_client_connect_async(&client->base_tcp, hostname, service, err)) terom@85: goto error; terom@2: terom@139: // done, wait for the connect to complete terom@180: *transport_ptr = SSL_CLIENT_TRANSPORT(client); terom@5: terom@5: return SUCCESS; terom@5: terom@5: error: terom@29: // cleanup terom@180: ssl_client_destroy(client); terom@5: terom@5: return ERROR_CODE(err); terom@2: } terom@2: terom@140: