mirror of https://gerrit.osmocom.org/osmo-pcap
352 lines
9.3 KiB
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;
|
|
}
|