/* * osmo-pcap TLS code * * (C) 2016 by Holger Hans Peter Freyther * 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 . * */ #include #include #include #include #include #include #include #define CHECK_RC(rc, str) \ if (rc != 0) { \ LOGP(DTLS, LOGL_ERROR, "%s with rc=%d\n", str, rc); \ exit(1); \ } static int generate_dh_params(struct osmo_pcap_server *server) { int rc; unsigned int bits = gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH, GNUTLS_SEC_PARAM_HIGH); LOGP(DTLS, LOGL_NOTICE, "Going to create DH params for %d bits\n", bits); /* allocate it */ rc = gnutls_dh_params_init (&server->dh_params); if (rc != GNUTLS_E_SUCCESS) { LOGP(DTLS, LOGL_ERROR, "Failed to allocate DH params rc=%d\n", rc); server->dh_params_allocated = false; return rc; } /* generate and check */ rc = gnutls_dh_params_generate2 (server->dh_params, bits); if (rc == GNUTLS_E_SUCCESS) server->dh_params_allocated = true; else { LOGP(DTLS, LOGL_ERROR, "Failed to generate DH params rc=%d\n", rc); server->dh_params_allocated = false; gnutls_dh_params_deinit(server->dh_params); } return rc; } void osmo_tls_dh_load(struct osmo_pcap_server *server) { gnutls_datum_t data; int rc; /* free it before we start */ if (server->dh_params_allocated) { gnutls_dh_params_deinit(server->dh_params); server->dh_params_allocated = false; } /* check if we have all data */ if (!server->tls_dh_pkcs3) { LOGP(DTLS, LOGL_ERROR, "Can not generate missing pkcs3=%p\n", server->tls_dh_pkcs3); return; } /* initialize it again */ rc = gnutls_dh_params_init (&server->dh_params); if (rc != GNUTLS_E_SUCCESS) { LOGP(DTLS, LOGL_ERROR, "Failed to allocate DH params rc=%d\n", rc); server->dh_params_allocated = false; return; } /* load prime and generator */ rc = gnutls_load_file(server->tls_dh_pkcs3, &data); if (rc != GNUTLS_E_SUCCESS) { LOGP(DTLS, LOGL_ERROR, "Failed to load DH params from=%s rc=%d\n", server->tls_dh_pkcs3, rc); gnutls_dh_params_deinit(server->dh_params); return; } rc = gnutls_dh_params_import_pkcs3(server->dh_params, &data, GNUTLS_X509_FMT_PEM); gnutls_free(data.data); if (rc != GNUTLS_E_SUCCESS) { LOGP(DTLS, LOGL_ERROR, "Failed to import DH params rc=%d\n", rc); gnutls_dh_params_deinit(server->dh_params); return; } /* done */ server->dh_params_allocated = true; } void osmo_tls_dh_generate(struct osmo_pcap_server *server) { if (server->dh_params_allocated) gnutls_dh_params_deinit(server->dh_params); generate_dh_params(server); } 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); } void osmo_tls_server_init(struct osmo_pcap_server *server) { int rc; if (server->dh_params_allocated) return; rc = generate_dh_params(server); CHECK_RC(rc, "dh params failed"); } 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 = OSMO_FD_WRITE | OSMO_FD_READ; else tls_session->wqueue->bfd.when = OSMO_FD_READ; tls_session->need_handshake = false; release_keys(tls_session); if (tls_session->handshake_done) 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; if (sess->read) return sess->read(sess); 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 &= ~OSMO_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 |= OSMO_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 & OSMO_FD_READ) { int rc = tls_read(sess); if (rc <= 0) { sess->error(sess); return rc; } } if (what & OSMO_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_conn *conn) { struct osmo_tls_session *sess = &conn->tls_session; gnutls_datum_t data; int rc; if (!conn->tls_client_cert || !conn->tls_client_key) { LOGP(DTLS, LOGL_DEBUG, "Skipping x509 client cert %p %p\n", conn->tls_client_cert, conn->tls_client_key); return 0; } rc = gnutls_load_file(conn->tls_client_cert, &data); if (rc < 0) { LOGP(DTLS, LOGL_ERROR, "Failed to load file=%s rc=%d\n", conn->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", conn->tls_client_cert, rc); return -1; } sess->pcert_alloc = true; /* copied to RAM.. nothing we can do about it */ rc = gnutls_load_file(conn->tls_client_key, &data); if (rc < 0) { LOGP(DTLS, LOGL_ERROR, "Failed to load file=%s rc=%d\n", conn->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", conn->tls_client_key, rc); release_keys(sess); return -1; } sess->privk_alloc = true; return 0; } size_t osmo_tls_pending(struct osmo_tls_session *sess) { return gnutls_record_check_pending(sess->session); } bool osmo_tls_init_server_session(struct osmo_pcap_conn *conn, struct osmo_pcap_server *server) { struct osmo_tls_session *sess = &conn->tls_session; struct osmo_wqueue *wq = &conn->rem_wq; int rc; gnutls_global_set_log_level(server->tls_log_level); memset(sess, 0, sizeof(*sess)); sess->in_use = sess->anon_alloc = sess->cert_alloc = false; rc = gnutls_init(&sess->session, GNUTLS_SERVER | 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 (server->tls_priority) { const char *err; rc = gnutls_priority_set_direct(sess->session, server->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_server_credentials(&sess->anon_serv_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_serv_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 */ if (server->dh_params_allocated) { gnutls_anon_set_server_dh_params(sess->anon_serv_cred, server->dh_params); gnutls_certificate_set_dh_params(sess->cert_cred, server->dh_params); } if (server->tls_allow_anon) gnutls_credentials_set(sess->session, GNUTLS_CRD_ANON, sess->anon_serv_cred); if (server->tls_allow_x509) gnutls_credentials_set(sess->session, GNUTLS_CRD_CERTIFICATE, sess->cert_cred); if (server->tls_capath) { rc = gnutls_certificate_set_x509_trust_file( sess->cert_cred, server->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", server->tls_capath, rc); osmo_tls_release(sess); return false; } } if (server->tls_crlfile) { rc = gnutls_certificate_set_x509_crl_file( sess->cert_cred, server->tls_crlfile, GNUTLS_X509_FMT_PEM); if (rc != GNUTLS_E_SUCCESS) { LOGP(DTLS, LOGL_ERROR, "Failed to load crlfile from path=%s rc=%d\n", server->tls_crlfile, rc); osmo_tls_release(sess); return false; } } if (server->tls_server_cert && server->tls_server_key) { rc = gnutls_certificate_set_x509_key_file( sess->cert_cred, server->tls_server_cert, server->tls_server_key, GNUTLS_X509_FMT_PEM); if (rc != GNUTLS_E_SUCCESS) { LOGP(DTLS, LOGL_ERROR, "Failed to load crt/key from path=%s/%s rc=%d\n", server->tls_server_cert, server->tls_server_key, rc); osmo_tls_release(sess); return false; } } #warning "TODO client certificates" gnutls_transport_set_int(sess->session, wq->bfd.fd); gnutls_handshake_set_timeout(sess->session, GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT); osmo_fd_setup(&wq->bfd, wq->bfd.fd, OSMO_FD_READ | OSMO_FD_WRITE, osmo_tls_client_bfd_cb, sess, 0); sess->need_handshake = true; sess->wqueue = wq; return true; } bool osmo_tls_init_client_session(struct osmo_pcap_client_conn *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); osmo_fd_setup(&wq->bfd, wq->bfd.fd, OSMO_FD_READ | OSMO_FD_WRITE, osmo_tls_client_bfd_cb, sess, 0); 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->anon_serv_alloc) gnutls_anon_free_server_credentials(session->anon_serv_cred); if (session->cert_alloc) gnutls_certificate_free_credentials(session->cert_cred); session->in_use = false; }