osmo-pcap/src/osmo_tls.c

352 lines
9.3 KiB
C

/*
* 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;
}