Skip to content

Commit 2570719

Browse files
committed
libcami: Add support for native encrypted sessions using OpenSSL.
While a previous commit allows for encrypted AMI connections to be established using application code and custom file descriptors, this may not fit the architecture of all applications, so also add embedded TLS support using OpenSSL. TLS support is enabled by default in the Makefile, but can be easily disabled if needed or desired.
1 parent 5f42f99 commit 2570719

5 files changed

Lines changed: 175 additions & 7 deletions

File tree

.github/workflows/main.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,26 @@ jobs:
5050
runs-on: ubuntu-24.04
5151
name: Fedora 42
5252
container: fedora:42
53+
steps:
54+
- name: Checkout
55+
uses: actions/checkout@v4
56+
- name: Build CAMI
57+
run: |
58+
dnf install -y make gcc openssl-devel
59+
make
60+
make install
61+
make examples
62+
fedora-42-nossl:
63+
runs-on: ubuntu-24.04
64+
name: Fedora 42, no SSL
65+
container: fedora:42
5366
steps:
5467
- name: Checkout
5568
uses: actions/checkout@v4
5669
- name: Build CAMI
5770
run: |
5871
dnf install -y make gcc
72+
sed -i 's|HAVE_OPENSSL = 1|HAVE_OPENSSL = 0|' Makefile
5973
make
6074
make install
6175
make examples

Makefile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,21 @@
66
# Naveen Albert <asterisk@phreaknet.org>
77
#
88

9+
# Compile the library with TLS support, using OpenSSL
10+
# If you don't have OpenSSL, you can disable TLS support by setting this to 0
11+
HAVE_OPENSSL = 1
12+
913
CC = gcc
1014
# Without -fPIC in CFLAGS, linking fails on FreeBSD: relocation R_X86_64_32 against `.rodata.str1.8' can not be used when making a shared object; recompile with -fPIC
1115
CFLAGS = -Wall -Werror -Wno-unused-parameter -Wextra -Wstrict-prototypes -Wmissing-prototypes -Wdeclaration-after-statement -Wmissing-declarations -Wmissing-format-attribute -Wformat=2 -Wshadow -std=gnu99 -pthread -O3 -g -Wstack-protector -fno-omit-frame-pointer -D_FORTIFY_SOURCE=2 -fPIC -I.
1216
EXE = cami
1317
SAMPEXES = simpleami amicli
1418
LIBNAME = lib$(EXE).so
1519
LIBS = -lm -ldl
20+
ifeq ($(HAVE_OPENSSL),1)
21+
CFLAGS += -DHAVE_OPENSSL
22+
LIBS += -lssl -lcrypto
23+
endif
1624
RM = rm -f
1725
INSTALL = install
1826

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ The program is named CAMI as it takes its cue from AMI libraries for other langu
1111

1212
You may simply statically compile your program with `cami.c` and include the CAMI header files where needed.
1313

14-
Alternately, you may build and install the library by running `make install`.
14+
Alternately, you may build and install the library by running `make install` (this is the recommended approach).
15+
16+
`cami` links with OpenSSL by default for TLS support, but this is optional. You can disable this by setting `HAVE_OPENSSL` to 0 in the Makefile.
1517

1618
## Usage
1719

cami.c

Lines changed: 144 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,17 @@
4141

4242
#include "include/cami.h"
4343

44+
#ifdef HAVE_OPENSSL
45+
#include <openssl/opensslv.h>
46+
#include <openssl/bio.h>
47+
#include <openssl/ssl.h>
48+
#include <openssl/err.h>
49+
static char root_certs[84] = "/etc/ssl/certs/ca-certificates.crt"; /* Path on Debian */
50+
#endif
51+
4452
/*! \brief Default Asterisk Manager Interface port is 5038 */
45-
#define AMI_PORT 5038
53+
#define AMI_PORT_UNENCRYPTED 5038
54+
#define AMI_PORT_ENCRYPTED 5039
4655
/*! \brief This is the finale to any message exchange. */
4756
#define AMI_EOM "\r\n\r\n"
4857

@@ -60,7 +69,7 @@
6069
}
6170

6271
#define ami_warning(ami, fmt, ...) ami_debug(ami, 1, "WARNING: " fmt, ## __VA_ARGS__)
63-
#define ami_error(ami, fmt, ...) ami_debug(ami, 1, "WARNING: " fmt, ## __VA_ARGS__)
72+
#define ami_error(ami, fmt, ...) ami_debug(ami, 1, "ERROR: " fmt, ## __VA_ARGS__)
6473

6574
#define strlen_zero(s) ((!s || *s == '\0'))
6675
#define ltrim(s) while (isspace(*s)) s++;
@@ -81,6 +90,9 @@ struct ami_session {
8190
struct ami_response *current_response;
8291
int ami_rfd; /* Read file descriptor (typically socket fd) */
8392
int ami_wfd; /* Write file descriptor (typically socket fd) */
93+
#ifdef HAVE_OPENSSL
94+
SSL *ssl;
95+
#endif
8496
int debugfd;
8597
int debug_level;
8698
int ami_pipe[2]; /* Pipe for sending actions to Asterisk */
@@ -157,6 +169,13 @@ static void ami_cleanup(struct ami_session *ami)
157169
ami->ami_wfd = -1;
158170
}
159171

172+
#ifdef HAVE_OPENSSL
173+
if (ami->ssl) {
174+
SSL_shutdown(ami->ssl);
175+
SSL_free(ami->ssl);
176+
}
177+
#endif
178+
160179
ami->loggedin = 0;
161180
ami->tx = ami->rx = 0;
162181
if (ami->current_response) {
@@ -165,6 +184,91 @@ static void ami_cleanup(struct ami_session *ami)
165184
}
166185
}
167186

187+
#ifdef HAVE_OPENSSL
188+
static int start_tls(struct ami_session *ami, const char *hostname)
189+
{
190+
SSL_CTX *ctx;
191+
X509 *server_cert;
192+
long verify_result;
193+
char *str;
194+
195+
OpenSSL_add_ssl_algorithms();
196+
SSL_load_error_strings();
197+
198+
ctx = SSL_CTX_new(TLS_client_method());
199+
if (!ctx) {
200+
ami_error(ami, "Failed to setup new SSL context\n");
201+
return -1;
202+
}
203+
204+
ami->ssl = SSL_new(ctx);
205+
if (!ami->ssl) {
206+
ami_error(ami, "Failed to create new SSL\n");
207+
SSL_CTX_free(ctx);
208+
return -1;
209+
}
210+
211+
if (SSL_set_fd(ami->ssl, ami->ami_rfd) != 1) {
212+
ami_error(ami, "Failed to connect SSL: %s\n", ERR_error_string(ERR_get_error(), NULL));
213+
goto sslcleanup;
214+
}
215+
216+
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
217+
SSL_CTX_set_verify_depth(ctx, 4);
218+
if (SSL_CTX_load_verify_locations(ctx, root_certs, NULL) != 1) {
219+
ami_error(ami, "Failed to load root certs from %s: %s\n", root_certs, ERR_error_string(ERR_get_error(), NULL));
220+
goto sslcleanup;
221+
}
222+
223+
/* SNI */
224+
if (SSL_set_tlsext_host_name(ami->ssl, hostname) != 1) {
225+
ami_warning(ami, "Failed to set SNI for TLS connection\n");
226+
}
227+
228+
if (SSL_connect(ami->ssl) == -1) {
229+
ami_error(ami, "Failed to connect SSL: %s\n", ERR_error_string(ERR_get_error(), NULL));
230+
}
231+
232+
/* Verify cert */
233+
#if defined(OPENSSL_VERSION_MAJOR) && OPENSSL_VERSION_MAJOR >= 3
234+
server_cert = SSL_get1_peer_certificate(ami->ssl);
235+
#else
236+
server_cert = SSL_get_peer_certificate(ami->ssl);
237+
#endif
238+
if (!server_cert) {
239+
ami_error(ami, "Failed to get peer certificate\n");
240+
goto sslcleanup;
241+
}
242+
str = X509_NAME_oneline(X509_get_subject_name(server_cert), 0, 0);
243+
if (!str) {
244+
ami_error(ami, "Failed to get peer certificate\n");
245+
goto sslcleanup;
246+
}
247+
OPENSSL_free(str);
248+
str = X509_NAME_oneline(X509_get_issuer_name(server_cert), 0, 0);
249+
if (!str) {
250+
ami_error(ami, "Failed to get peer certificate\n");
251+
goto sslcleanup;
252+
}
253+
OPENSSL_free(str);
254+
X509_free(server_cert);
255+
verify_result = SSL_get_verify_result(ami->ssl);
256+
if (verify_result != X509_V_OK) {
257+
ami_warning(ami, "SSL verify failed: %ld (%s)\n", verify_result, X509_verify_cert_error_string(verify_result));
258+
goto sslcleanup;
259+
}
260+
261+
SSL_CTX_free(ctx); /* ctx will still have a ref count of 1 after this, to be cleaned up in ssl_close by SSL_free */
262+
return 0;
263+
264+
sslcleanup:
265+
SSL_CTX_free(ctx);
266+
SSL_free(ami->ssl);
267+
ami->ssl = NULL;
268+
return -1;
269+
}
270+
#endif /* HAVE_OPENSSL */
271+
168272
static void *ami_loop(void *varg)
169273
{
170274
int res, got_id = 0, response_pending = 0, event_pending = 0;
@@ -200,6 +304,11 @@ static void *ami_loop(void *varg)
200304
}
201305
/* Data from AMI to deliver to consumer? */
202306
if (fds[0].revents) {
307+
#ifdef HAVE_OPENSSL
308+
if (ami->ssl) {
309+
res = SSL_read(ami->ssl, readinbuf, AMI_BUFFER_SIZE - 2 - (readinbuf - inbuf));
310+
} else
311+
#endif
203312
res = read(ami->ami_rfd, readinbuf, AMI_BUFFER_SIZE - 2 - (readinbuf - inbuf));
204313
if (res < 1) {
205314
if (res < 0) {
@@ -356,6 +465,11 @@ static void *ami_loop(void *varg)
356465
ami_debug(ami, 1, "read returned %d\n", res);
357466
break;
358467
}
468+
#ifdef HAVE_OPENSSL
469+
if (ami->ssl) {
470+
res = SSL_write(ami->ssl, outbuf, res);
471+
} else
472+
#endif
359473
res = write(ami->ami_wfd, outbuf, res);
360474
if (res < 1) {
361475
ami_warning(ami, "write(%d) returned %d: %s\n", ami->ami_wfd, res, strerror(errno));
@@ -450,7 +564,7 @@ struct ami_session *ami_session_from_fd(int rfd, int wfd, void (*callback)(struc
450564
return ami;
451565
}
452566

453-
struct ami_session *ami_connect(const char *hostname, int port, void (*callback)(struct ami_session *ami, struct ami_event *event), void (*dis_callback)(struct ami_session *ami))
567+
static struct ami_session *_ami_connect(const char *hostname, int port, int tls, void (*callback)(struct ami_session *ami, struct ami_event *event), void (*dis_callback)(struct ami_session *ami))
454568
{
455569
int fd;
456570
struct sockaddr_in saddr;
@@ -463,10 +577,10 @@ struct ami_session *ami_connect(const char *hostname, int port, void (*callback)
463577

464578
memset(&saddr, 0, sizeof(saddr));
465579
if (!port) {
466-
port = AMI_PORT;
580+
port = tls ? AMI_PORT_ENCRYPTED : AMI_PORT_UNENCRYPTED;
467581
}
468582

469-
ami_debug(ami, 1, "Initiating AMI connection on port %d\n", port);
583+
ami_debug(ami, 1, "Initiating AMI connection to %s://%s:%d\n", tls ? "tls" : "tcp", hostname, port);
470584

471585
fd = socket(AF_INET, SOCK_STREAM, 0);
472586
if (fd < 0) {
@@ -519,7 +633,6 @@ struct ami_session *ami_connect(const char *hostname, int port, void (*callback)
519633
ami_cleanup(ami);
520634
goto cleanup;
521635
}
522-
523636
freeaddrinfo(res);
524637
} else {
525638
ami_error(ami, "host %s not valid\n", hostname);
@@ -530,6 +643,16 @@ struct ami_session *ami_connect(const char *hostname, int port, void (*callback)
530643

531644
ami->ami_callback = callback;
532645
ami->disconnected_callback = dis_callback;
646+
647+
#ifdef HAVE_OPENSSL
648+
/* Need to start SSL before launching threads and such */
649+
if (tls && start_tls(ami, hostname)) {
650+
ami_error(ami, "Failed to set up TLS\n");
651+
ami_cleanup(ami);
652+
goto cleanup;
653+
}
654+
#endif
655+
533656
if (common_init(ami)) {
534657
ami_cleanup(ami);
535658
goto cleanup;
@@ -542,6 +665,21 @@ struct ami_session *ami_connect(const char *hostname, int port, void (*callback)
542665
return NULL;
543666
}
544667

668+
struct ami_session *ami_connect(const char *hostname, int port, void (*callback)(struct ami_session *ami, struct ami_event *event), void (*dis_callback)(struct ami_session *ami))
669+
{
670+
return _ami_connect(hostname, port, 0, callback, dis_callback);
671+
}
672+
673+
struct ami_session *ami_ssl_connect(const char *hostname, int port, void (*callback)(struct ami_session *ami, struct ami_event *event), void (*dis_callback)(struct ami_session *ami))
674+
{
675+
#ifdef HAVE_OPENSSL
676+
return _ami_connect(hostname, port, 1, callback, dis_callback);
677+
#else
678+
errno = EPROTONOSUPPORT;
679+
return NULL;
680+
#endif
681+
}
682+
545683
void ami_set_callback_data(struct ami_session *ami, void *data)
546684
{
547685
ami->cb_data = data;

include/cami.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ struct ami_session *ami_session_from_fd(int rfd, int wfd, void (*callback)(struc
9696
*/
9797
struct ami_session *ami_connect(const char *hostname, int port, void (*callback)(struct ami_session *ami, struct ami_event *event), void (*dis_callback)(struct ami_session *ami));
9898

99+
/*!
100+
* \brief Same as ami_connect, but establish an encrypted connection using TLS, and if port is 0, it will default to 5039.
101+
* \note This function only works if HAVE_OPENSSL is enabled in Makefile.
102+
*/
103+
struct ami_session *ami_ssl_connect(const char *hostname, int port, void (*callback)(struct ami_session *ami, struct ami_event *event), void (*dis_callback)(struct ami_session *ami));
104+
99105
/*!
100106
* \brief Set callback data
101107
* \param ami

0 commit comments

Comments
 (0)