| /* |
| * osmo-pcap TLS code |
| * |
| * (C) 2016 by Holger Hans Peter Freyther <holger@moiji-mobile.com> |
| * All Rights Reserved |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU Affero General Public License as published by |
| * the Free Software Foundation; either version 3 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU Affero General Public License for more details. |
| * |
| * You should have received a copy of the GNU Affero General Public License |
| * along with this program. If not, see <http://www.gnu.org/licenses/>. |
| * |
| */ |
| |
| #include <osmo-pcap/osmo_tls.h> |
| #include <osmo-pcap/osmo_pcap_client.h> |
| #include <osmo-pcap/common.h> |
| |
| #include <osmocom/core/write_queue.h> |
| #include <osmocom/core/talloc.h> |
| |
| #include <string.h> |
| |
| #define CHECK_RC(rc, str) \ |
| if (rc != 0) { \ |
| LOGP(DTLS, LOGL_ERROR, "%s with rc=%d\n", str, rc); \ |
| exit(1); \ |
| } |
| |
| |
| static int cert_callback(gnutls_session_t tls_session, |
| const gnutls_datum_t * req_ca_rdn, int nreqs, |
| const gnutls_pk_algorithm_t * sign_algos, |
| int sign_algos_length, gnutls_pcert_st ** pcert, |
| unsigned int *pcert_length, gnutls_privkey_t * pkey) |
| { |
| struct osmo_tls_session *sess = gnutls_session_get_ptr(tls_session); |
| gnutls_certificate_type_t type; |
| |
| LOGP(DTLS, LOGL_DEBUG, "cert callback from server\n"); |
| type = gnutls_certificate_type_get(tls_session); |
| if (type != GNUTLS_CRT_X509) |
| return -1; |
| |
| *pcert_length = 1; |
| *pcert = &sess->pcert; |
| *pkey = sess->privk; |
| return 0; |
| } |
| |
| static void tls_log_func(int level, const char *str) |
| { |
| LOGP(DTLS, LOGL_DEBUG, "GNUtls: |<%d>| %s", level, str); |
| } |
| |
| static int verify_cert_cb(gnutls_session_t session) |
| { |
| const char *hostname; |
| unsigned int status; |
| int ret; |
| |
| hostname = gnutls_session_get_ptr(session); |
| ret = gnutls_certificate_verify_peers3(session, |
| hostname, &status); |
| if (ret != 0) |
| return GNUTLS_E_CERTIFICATE_ERROR; |
| if (status != 0) |
| return GNUTLS_E_CERTIFICATE_ERROR; |
| return 0; |
| } |
| |
| static void release_keys(struct osmo_tls_session *sess) |
| { |
| if (sess->pcert_alloc) { |
| gnutls_pcert_deinit(&sess->pcert); |
| sess->pcert_alloc = false; |
| } |
| if (sess->privk_alloc) { |
| gnutls_privkey_deinit(sess->privk); |
| sess->privk_alloc = false; |
| } |
| } |
| |
| void osmo_tls_init(void) |
| { |
| int rc; |
| rc = gnutls_global_init(); |
| CHECK_RC(rc, "init failed"); |
| gnutls_global_set_log_function(tls_log_func); |
| } |
| |
| static int need_handshake(struct osmo_tls_session *tls_session) |
| { |
| int rc; |
| |
| rc = gnutls_handshake(tls_session->session); |
| if (rc == 0) { |
| /* handshake is done. start writing if we are allowed to */ |
| LOGP(DTLS, LOGL_NOTICE, "TLS handshake done.\n"); |
| if (!llist_empty(&tls_session->wqueue->msg_queue)) |
| tls_session->wqueue->bfd.when = BSC_FD_WRITE | BSC_FD_READ; |
| else |
| tls_session->wqueue->bfd.when = BSC_FD_READ; |
| tls_session->need_handshake = false; |
| release_keys(tls_session); |
| tls_session->handshake_done(tls_session); |
| } else if (rc == GNUTLS_E_AGAIN || rc == GNUTLS_E_INTERRUPTED) { |
| LOGP(DTLS, LOGL_DEBUG, "rc=%d will wait for writable again.\n", rc); |
| } else if (gnutls_error_is_fatal(rc)) { |
| /* it failed for good.. */ |
| LOGP(DTLS, LOGL_ERROR, "handshake failed rc=%d str=%s\n", |
| rc, gnutls_strerror(rc)); |
| tls_session->wqueue->bfd.when = 0; |
| tls_session->error(tls_session); |
| } |
| return 0; |
| } |
| |
| static int tls_read(struct osmo_tls_session *sess) |
| { |
| char buf[1024]; |
| int rc; |
| |
| memset(buf, 0, sizeof(buf)); |
| rc = gnutls_record_recv(sess->session, buf, sizeof(buf) - 1); |
| return rc; |
| } |
| |
| static int tls_write(struct osmo_tls_session *sess) |
| { |
| int rc; |
| sess->wqueue->bfd.when &= ~BSC_FD_WRITE; |
| |
| if (llist_empty(&sess->wqueue->msg_queue)) |
| return 0; |
| |
| if (sess->need_resend) { |
| rc = gnutls_record_send(sess->session, NULL, 0); |
| } else { |
| struct msgb *msg; |
| msg = (struct msgb *) sess->wqueue->msg_queue.next; |
| rc = gnutls_record_send(sess->session, msg->data, msg->len); |
| } |
| |
| if (rc > 0) { |
| sess->wqueue->current_length -= 1; |
| sess->need_resend = false; |
| struct msgb *msg = msgb_dequeue(&sess->wqueue->msg_queue); |
| msgb_free(msg); |
| } else if (rc == GNUTLS_E_INTERRUPTED || rc == GNUTLS_E_AGAIN) { |
| sess->need_resend = true; |
| } else if (gnutls_error_is_fatal(rc)) { |
| return rc; |
| } |
| |
| if (sess->need_resend || !llist_empty(&sess->wqueue->msg_queue)) |
| sess->wqueue->bfd.when |= BSC_FD_WRITE; |
| return rc; |
| } |
| |
| int osmo_tls_client_bfd_cb(struct osmo_fd *fd, unsigned what) |
| { |
| struct osmo_tls_session *sess = fd->data; |
| |
| if (sess->need_handshake) |
| return need_handshake(sess); |
| |
| if (what & BSC_FD_READ) { |
| int rc = tls_read(sess); |
| if (rc <= 0) { |
| sess->error(sess); |
| return rc; |
| } |
| } |
| if (what & BSC_FD_WRITE) { |
| int rc = tls_write(sess); |
| if (rc < 0) { |
| sess->error(sess); |
| return rc; |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int load_keys(struct osmo_pcap_client *client) |
| { |
| struct osmo_tls_session *sess = &client->tls_session; |
| gnutls_datum_t data; |
| int rc; |
| |
| if (!client->tls_client_cert || !client->tls_client_key) { |
| LOGP(DTLS, LOGL_DEBUG, "Skipping x509 client cert %p %p\n", |
| client->tls_client_cert, client->tls_client_key); |
| return 0; |
| } |
| |
| |
| rc = gnutls_load_file(client->tls_client_cert, &data); |
| if (rc < 0) { |
| LOGP(DTLS, LOGL_ERROR, "Failed to load file=%s rc=%d\n", |
| client->tls_client_cert, rc); |
| return -1; |
| } |
| rc = gnutls_pcert_import_x509_raw(&sess->pcert, &data, GNUTLS_X509_FMT_PEM, 0); |
| gnutls_free(data.data); |
| if (rc < 0) { |
| LOGP(DTLS, LOGL_ERROR, "Failed to import file=%s rc=%d\n", |
| client->tls_client_cert, rc); |
| return -1; |
| } |
| sess->pcert_alloc = true; |
| |
| /* copied to RAM.. nothing we can do about it */ |
| rc = gnutls_load_file(client->tls_client_key, &data); |
| if (rc < 0) { |
| LOGP(DTLS, LOGL_ERROR, "Failed to load file=%s rc=%d\n", |
| client->tls_client_key, rc); |
| return -1; |
| } |
| gnutls_privkey_init(&sess->privk); |
| rc = gnutls_privkey_import_x509_raw(sess->privk, &data, GNUTLS_X509_FMT_PEM, NULL, 0); |
| gnutls_free(data.data); |
| if (rc < 0) { |
| LOGP(DTLS, LOGL_ERROR, "Failed to load file=%s rc=%d\n", |
| client->tls_client_key, rc); |
| release_keys(sess); |
| return -1; |
| } |
| sess->privk_alloc = true; |
| return 0; |
| } |
| |
| bool osmo_tls_init_client_session(struct osmo_pcap_client *client) |
| { |
| struct osmo_tls_session *sess = &client->tls_session; |
| struct osmo_wqueue *wq = &client->wqueue; |
| unsigned int status; |
| int rc; |
| |
| gnutls_global_set_log_level(client->tls_log_level); |
| |
| memset(sess, 0, sizeof(*sess)); |
| sess->in_use = sess->anon_alloc = sess->cert_alloc = false; |
| rc = gnutls_init(&sess->session, GNUTLS_CLIENT | GNUTLS_NONBLOCK); |
| if (rc != GNUTLS_E_SUCCESS) { |
| LOGP(DTLS, LOGL_ERROR, "gnutls_init failed with rc=%d\n", rc); |
| return false; |
| } |
| gnutls_session_set_ptr(sess->session, sess); |
| sess->in_use = true; |
| |
| /* use default or string */ |
| if (client->tls_priority) { |
| const char *err; |
| rc = gnutls_priority_set_direct(sess->session, client->tls_priority, &err); |
| } else { |
| rc = gnutls_set_default_priority(sess->session); |
| } |
| |
| if (rc != GNUTLS_E_SUCCESS) { |
| LOGP(DTLS, LOGL_ERROR, "def prio failed with rc=%d\n", rc); |
| osmo_tls_release(sess); |
| return false; |
| } |
| |
| /* allow username/password operation */ |
| rc = gnutls_anon_allocate_client_credentials(&sess->anon_cred); |
| if (rc != GNUTLS_E_SUCCESS) { |
| LOGP(DTLS, LOGL_ERROR, "Failed to allocate anon cred rc=%d\n", rc); |
| osmo_tls_release(sess); |
| return false; |
| } |
| sess->anon_alloc = true; |
| |
| /* x509 certificate handling */ |
| rc = gnutls_certificate_allocate_credentials(&sess->cert_cred); |
| if (rc != GNUTLS_E_SUCCESS) { |
| LOGP(DTLS, LOGL_ERROR, "Failed to allocate x509 cred rc=%d\n", rc); |
| osmo_tls_release(sess); |
| return false; |
| } |
| sess->cert_alloc = true; |
| |
| /* set the credentials now */ |
| gnutls_credentials_set(sess->session, GNUTLS_CRD_ANON, sess->anon_cred); |
| gnutls_credentials_set(sess->session, GNUTLS_CRD_CERTIFICATE, sess->cert_cred); |
| |
| if (client->tls_capath) { |
| rc = gnutls_certificate_set_x509_trust_file( |
| sess->cert_cred, client->tls_capath, GNUTLS_X509_FMT_PEM); |
| if (rc != GNUTLS_E_SUCCESS) { |
| LOGP(DTLS, LOGL_ERROR, "Failed to load capath from path=%s rc=%d\n", |
| client->tls_capath, rc); |
| osmo_tls_release(sess); |
| return false; |
| } |
| } |
| |
| if (load_keys(client) != 0) { |
| osmo_tls_release(sess); |
| return false; |
| } |
| |
| gnutls_certificate_set_retrieve_function2(sess->cert_cred, cert_callback); |
| |
| /* set the hostname if we have one */ |
| if (client->tls_hostname) |
| gnutls_server_name_set(sess->session, GNUTLS_NAME_DNS, |
| client->tls_hostname, strlen(client->tls_hostname)); |
| |
| /* do the verification */ |
| if (client->tls_verify) { |
| gnutls_certificate_set_verify_function(sess->cert_cred, verify_cert_cb); |
| gnutls_certificate_verify_peers3(sess->session, client->tls_hostname, &status); |
| } else |
| LOGP(DTLS, LOGL_NOTICE, "Not going to validate certs as configured\n"); |
| |
| gnutls_transport_set_int(sess->session, wq->bfd.fd); |
| gnutls_handshake_set_timeout(sess->session, |
| GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT); |
| wq->bfd.cb = osmo_tls_client_bfd_cb; |
| wq->bfd.data = sess; |
| wq->bfd.when = BSC_FD_READ | BSC_FD_WRITE; |
| sess->need_handshake = true; |
| sess->wqueue = wq; |
| return true; |
| } |
| |
| void osmo_tls_release(struct osmo_tls_session *session) |
| { |
| if (!session->in_use) |
| return; |
| |
| gnutls_deinit(session->session); |
| |
| release_keys(session); |
| |
| if (session->anon_alloc) |
| gnutls_anon_free_client_credentials(session->anon_cred); |
| if (session->cert_alloc) |
| gnutls_certificate_free_credentials(session->cert_cred); |
| session->in_use = false; |
| } |