diff options
Diffstat (limited to 'src/osmo_tls.c')
-rw-r--r-- | src/osmo_tls.c | 351 |
1 files changed, 351 insertions, 0 deletions
diff --git a/src/osmo_tls.c b/src/osmo_tls.c new file mode 100644 index 0000000..ae957e6 --- /dev/null +++ b/src/osmo_tls.c @@ -0,0 +1,351 @@ +/* + * 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; +} |