Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
# make library-reconnect-smoke on-target mco_AutoReconnect test (broker restart mid-run)
# make fetch-amissl-sdk fetch the AmiSSL v5 SDK (needed for Amiga-side TLS support)
# make library-tls-smoke on-target mco_TLS test - local-only, needs an amibake image
# make library-cafile-smoke on-target mco_CAFile test - local-only, needs an amibake image
# make clean
#
# The core is portable C99, so `test` and `cli` build with any host compiler.
Expand Down Expand Up @@ -407,6 +408,16 @@ libtls-m68k: library-headers | $(BUILD)/.dir
library-tls-smoke: library libtls-m68k cli
sh tests/library/tls-run.sh

# --- m68k: on-target mco_CAFile end-to-end smoke test (issue #13) ---
# Same shape/local-only rationale as libtls-m68k/library-tls-smoke - see
# tests/library/libcafile.c and cafile-run.sh's own banners (the latter
# also explains why this one seeds Copperline's RTC).
libcafile-m68k: library-headers | $(BUILD)/.dir
$(M68K_CC) $(M68K_CFLAGS) -I$(LIB_INCDIR) tests/library/libcafile.c -o $(BUILD)/libcafile

library-cafile-smoke: library libcafile-m68k
sh tests/library/cafile-run.sh

# --- m68k: on-target check that examples/pubexample.c actually works ---
# Real Copperline boot (CI/release gate): stages build/pubexample and
# build/mqtt.library into a throwaway boot volume with Copperline's
Expand Down
23 changes: 15 additions & 8 deletions midge.readme
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,28 @@ This release ships:
mqtt_pub/mqtt_sub and their -static counterparts all follow the same
AmigaDOS conventions (ReadArgs templates, Ctrl-C to stop) and speak plain
MQTT 3.1.1 over TCP. mqtt_pub/mqtt_sub are backed by mqtt.library and
support QoS 0 and QoS 1 publish; mqtt_pub-static/mqtt_sub-static talk to
bsdsocket.library directly with no library install required, but
mqtt_pub-static only supports QoS 0 publish. mqtt.library itself also
exposes an optional auto-reconnect mode to programs written against it
directly. See https://sidick.github.io/midge/mqtt-library/ for the full
mqtt.library guide.
support QoS 0 and QoS 1 publish, plus TLS via AmiSSL (TLS/TLSINSECURE -
needs AmiSSL installed separately, see Requirements below);
mqtt_pub-static/mqtt_sub-static talk to bsdsocket.library directly with no
library install required, but mqtt_pub-static only supports QoS 0 publish
and neither -static tool supports TLS. mqtt.library itself also exposes an
optional auto-reconnect mode to programs written against it directly. See
https://sidick.github.io/midge/mqtt-library/ for the full mqtt.library
guide.

Planned for future releases: TLS via AmiSSL, and a ReAction dashboard
application with Home Assistant MQTT discovery.
Planned for future releases: a ReAction dashboard application with Home
Assistant MQTT discovery.

Requirements
------------
- AmigaOS 3.1+ (3.2 is the reference platform), 68020 or better.
- A TCP/IP stack providing bsdsocket.library (Roadshow, AmiTCP, Miami,
or an emulator-provided stack).
- For TLS (mqtt_pub/mqtt_sub's TLS/TLSINSECURE switches): AmiSSL 5.x
(https://github.com/jens-maus/amissl), installed separately - not
bundled with this archive. Software TLS is CPU-intensive; a genuinely
stock, unaccelerated 68020 may see intermittent failures under it -
see https://sidick.github.io/midge/CLI-Reference/ for details.

Quick start
-----------
Expand Down
11 changes: 7 additions & 4 deletions src/amiga/args.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,21 @@

#define PUB_TEMPLATE \
"HOST/A,PORT/N/K,TOPIC/A,MESSAGE/K,FILE/K,QOS/N/K,CLIENTID/K,USER/K," \
"PASSWORD/K,KEEPALIVE/N/K,RETAIN/S,VERBOSE/S,TLS/S,TLSINSECURE/S"
"PASSWORD/K,KEEPALIVE/N/K,RETAIN/S,VERBOSE/S,TLS/S,TLSINSECURE/S," \
"CAFILE/K"
#define SUB_TEMPLATE \
"HOST/A,PORT/N/K,TOPIC/A,QOS/N/K,CLIENTID/K,USER/K,PASSWORD/K," \
"KEEPALIVE/N/K,COUNT/N/K,VERBOSE/S,TLS/S,TLSINSECURE/S"
"KEEPALIVE/N/K,COUNT/N/K,VERBOSE/S,TLS/S,TLSINSECURE/S,CAFILE/K"

enum {
PUB_HOST, PUB_PORT, PUB_TOPIC, PUB_MESSAGE, PUB_FILE, PUB_QOS,
PUB_CLIENTID, PUB_USER, PUB_PASSWORD, PUB_KEEPALIVE, PUB_RETAIN,
PUB_VERBOSE, PUB_TLS, PUB_TLSINSECURE, PUB_NARGS
PUB_VERBOSE, PUB_TLS, PUB_TLSINSECURE, PUB_CAFILE, PUB_NARGS
};
enum {
SUB_HOST, SUB_PORT, SUB_TOPIC, SUB_QOS, SUB_CLIENTID, SUB_USER,
SUB_PASSWORD, SUB_KEEPALIVE, SUB_COUNT, SUB_VERBOSE, SUB_TLS,
SUB_TLSINSECURE, SUB_NARGS
SUB_TLSINSECURE, SUB_CAFILE, SUB_NARGS
};

static struct RDArgs *g_rdargs;
Expand Down Expand Up @@ -67,6 +68,7 @@ int amiga_parse_args(int is_pub, tool_opts *opts)
opts->tls_insecure = args[PUB_TLSINSECURE] ? 1 : 0;
if (opts->tls_insecure)
opts->tls = 1;
opts->ca_file = (const char *)args[PUB_CAFILE];

if (!opts->message && !opts->file) {
fprintf(stderr, "mqtt_pub: MESSAGE or FILE is required\n");
Expand Down Expand Up @@ -98,6 +100,7 @@ int amiga_parse_args(int is_pub, tool_opts *opts)
opts->tls_insecure = args[SUB_TLSINSECURE] ? 1 : 0;
if (opts->tls_insecure)
opts->tls = 1;
opts->ca_file = (const char *)args[SUB_CAFILE];
}

if (opts->port == 0)
Expand Down
1 change: 1 addition & 0 deletions src/amiga/pub_main_lib.c
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ int main(void)
co.mco_AutoReconnect = FALSE;
co.mco_TLS = opts.tls ? TRUE : FALSE;
co.mco_TLSInsecure = opts.tls_insecure ? TRUE : FALSE;
co.mco_CAFile = (STRPTR)opts.ca_file;

if (opts.verbose)
printf("mqtt_pub: connecting to %s:%u\n", opts.host,
Expand Down
1 change: 1 addition & 0 deletions src/amiga/sub_main_lib.c
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ int main(void)
co.mco_AutoReconnect = FALSE;
co.mco_TLS = opts.tls ? TRUE : FALSE;
co.mco_TLSInsecure = opts.tls_insecure ? TRUE : FALSE;
co.mco_CAFile = (STRPTR)opts.ca_file;

if (opts.verbose)
printf("mqtt_sub: connecting to %s:%u\n", opts.host,
Expand Down
9 changes: 8 additions & 1 deletion src/amiga/transport_amissl.c
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@ static void amissl_close(void *vctx)

int transport_amissl_connect(mqtt_transport *out, amissl_ctx *ctx,
const char *host, uint16_t port,
int insecure_skip_verify)
int insecure_skip_verify,
const char *ca_file)
{
struct Library *SocketBase;
struct Library *AmiSSLMasterBase;
Expand Down Expand Up @@ -290,6 +291,12 @@ int transport_amissl_connect(mqtt_transport *out, amissl_ctx *ctx,
NULL);
if (!SSL_CTX_set_default_verify_paths(ctx->ssl_ctx))
goto fail_ctx;
/* Extra trust anchor for a private CA (issue #13) - added on top
* of the AmiSSL: cert store above, not instead of it, so a broker
* behind a normal public CA still verifies too. */
if (ca_file != NULL &&
!SSL_CTX_load_verify_locations(ctx->ssl_ctx, ca_file, NULL))
goto fail_ctx;
}

ctx->ssl = SSL_new(ctx->ssl_ctx);
Expand Down
9 changes: 7 additions & 2 deletions src/amiga/transport_amissl.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ typedef struct {
* mask, same discipline as transport_bsdsocket_connect). Certificate and
* hostname verification are on by default; pass insecure_skip_verify
* nonzero to disable both (SSL_VERIFY_NONE) - for testing against
* self-signed brokers only, never for production use.
* self-signed brokers only, never for production use. `ca_file`, if
* non-NULL, additionally loads a PEM file as an extra trust anchor
* (SSL_CTX_load_verify_locations()) - for a private CA not in AmiSSL's
* bundled trust store (issue #13); ignored when insecure_skip_verify is
* set (nothing to verify against then).
*
* Requires an AmiSSL: assign (see amissl.library's install docs - the
* amissl package assigns it to SYS:Devs/AmiSSL) for InitAmiSSL()'s cert
Expand All @@ -57,6 +61,7 @@ typedef struct {
* handshake failure) - no resources leaked on any error path. */
int transport_amissl_connect(mqtt_transport *out, amissl_ctx *ctx_storage,
const char *host, uint16_t port,
int insecure_skip_verify);
int insecure_skip_verify,
const char *ca_file);

#endif
10 changes: 6 additions & 4 deletions src/host/args.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
int host_parse_args(int argc, char **argv, int is_pub, tool_opts *opts)
{
int ch;
const char *optstring = is_pub ? "h:p:t:m:f:q:i:u:P:k:rvsS"
: "h:p:t:q:i:u:P:k:C:vsS";
const char *optstring = is_pub ? "h:p:t:m:f:q:i:u:P:k:rvsSc:"
: "h:p:t:q:i:u:P:k:C:vsSc:";

memset(opts, 0, sizeof(*opts));
/* opts->port stays 0 (memset) until resolved after the option loop,
Expand All @@ -40,15 +40,17 @@ int host_parse_args(int argc, char **argv, int is_pub, tool_opts *opts)
case 'v': opts->verbose = 1; break;
case 's': opts->tls = 1; break;
case 'S': opts->tls = 1; opts->tls_insecure = 1; break;
case 'c': opts->ca_file = optarg; break;
default:
fprintf(stderr,
is_pub ? "usage: %s -h host [-p port] -t topic "
"(-m message | -f file) [-q qos] [-i id] "
"[-u user] [-P pass] [-k keepalive] [-r] [-s] "
"[-S] [-v]\n"
"[-S] [-c cafile] [-v]\n"
: "usage: %s -h host [-p port] -t topic "
"[-q qos] [-i id] [-u user] [-P pass] "
"[-k keepalive] [-C count] [-s] [-S] [-v]\n",
"[-k keepalive] [-C count] [-s] [-S] "
"[-c cafile] [-v]\n",
argv[0]);
return -1;
}
Expand Down
3 changes: 2 additions & 1 deletion src/host/pub_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ int main(int argc, char **argv)
if (opts.tls) {
connected = transport_openssl_connect(&transport, &ossl_ctx,
opts.host, opts.port,
opts.tls_insecure) == 0;
opts.tls_insecure,
opts.ca_file) == 0;
} else {
connected = transport_bsd_connect(&transport, &ctx, opts.host,
opts.port) == 0;
Expand Down
3 changes: 2 additions & 1 deletion src/host/sub_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ int main(int argc, char **argv)
if (opts.tls) {
connected = transport_openssl_connect(&transport, &ossl_ctx,
opts.host, opts.port,
opts.tls_insecure) == 0;
opts.tls_insecure,
opts.ca_file) == 0;
} else {
connected = transport_bsd_connect(&transport, &ctx, opts.host,
opts.port) == 0;
Expand Down
13 changes: 12 additions & 1 deletion src/host/transport_openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ static int tcp_connect(const char *host, uint16_t port)

int transport_openssl_connect(mqtt_transport *out, openssl_ctx *ctx,
const char *host, uint16_t port,
int insecure_skip_verify)
int insecure_skip_verify,
const char *ca_file)
{
int fd;
SSL_CTX *ssl_ctx;
Expand Down Expand Up @@ -151,6 +152,16 @@ int transport_openssl_connect(mqtt_transport *out, openssl_ctx *ctx,
close(fd);
return -1;
}
/* Extra trust anchor for a private CA (issue #13) - added on top
* of the system trust store above, not instead of it, so a broker
* behind a normal public CA still verifies too. */
if (ca_file != NULL &&
!SSL_CTX_load_verify_locations(ssl_ctx, ca_file, NULL)) {
fprintf(stderr, "mqtt: failed to load CA file %s\n", ca_file);
SSL_CTX_free(ssl_ctx);
close(fd);
return -1;
}
}

ssl = SSL_new(ssl_ctx);
Expand Down
12 changes: 8 additions & 4 deletions src/host/transport_openssl.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ typedef struct {
* and hostname verification are on by default (system trust store via
* SSL_CTX_set_default_verify_paths()); pass insecure_skip_verify nonzero to
* disable both (SSL_VERIFY_NONE) - for testing against self-signed brokers
* only, never for production use. Wires `out` up via `ctx` (caller-owned, no
* allocation). Returns 0 on success, -1 on failure (no resources leaked on
* any error path). */
* only, never for production use. `ca_file`, if non-NULL, additionally loads
* a PEM file as an extra trust anchor (SSL_CTX_load_verify_locations()) -
* for a private CA not in the system trust store (issue #13); ignored when
* insecure_skip_verify is set (nothing to verify against then). Wires `out`
* up via `ctx` (caller-owned, no allocation). Returns 0 on success, -1 on
* failure (no resources leaked on any error path). */
int transport_openssl_connect(mqtt_transport *out, openssl_ctx *ctx,
const char *host, uint16_t port,
int insecure_skip_verify);
int insecure_skip_verify,
const char *ca_file);

#endif
8 changes: 8 additions & 0 deletions src/library/include/libraries/mqtt.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ struct MqttConnectOpts {
unless mco_TLS is also TRUE. For testing
against self-signed or otherwise untrusted
brokers only - never for production use. */
STRPTR mco_CAFile; /* NUL-terminated path to a PEM file, loaded
as an extra trust anchor alongside
AmiSSL's bundled trust store - for a
broker behind a private CA that isn't in
it (issue #13). Ignored unless mco_TLS is
TRUE, and ignored if mco_TLSInsecure is
also TRUE (nothing to verify against
then). NULL = no extra trust anchor. */
BOOL mco_AutoReconnect; /* FALSE (default/zeroed struct) = today's
behaviour: an unexpected connection drop
(transport error, keepalive timeout) leaves
Expand Down
15 changes: 13 additions & 2 deletions src/library/mqtt.doc
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ mqtt.library/MQTT_CreateClient mqtt.library/MQTT_CreateClient
port).
opts - connect options (client id, credentials, keepalive,
clean-session, mco_AutoReconnect, mco_TLS,
mco_TLSInsecure); see struct MqttConnectOpts in
<libraries/mqtt.h>. May be NULL, which behaves like a
mco_TLSInsecure, mco_CAFile); see struct MqttConnectOpts
in <libraries/mqtt.h>. May be NULL, which behaves like a
zeroed struct (no client id, no credentials, no
keepalive, clean session, no auto-reconnect, no TLS).

Expand All @@ -214,6 +214,17 @@ mqtt.library/MQTT_CreateClient mqtt.library/MQTT_CreateClient
self-signed or otherwise untrusted brokers only, never for
production use.

mco_CAFile (issue #13) additionally trusts a PEM file's CA
alongside AmiSSL's bundled trust store - for a broker behind a
private CA that isn't in it. Certificate verification checks the
broker cert's validity dates against the Amiga's own system
clock, same as any TLS client - a system clock that's wrong
(common on real hardware with a dead or unset battery-backed
RTC) will make a perfectly good certificate look not-yet-valid
or expired and fail the handshake. Set the clock (SetClock,
IControl, or NTP via a suitable client) before relying on
certificate verification.

SEE ALSO
MQTT_Connect, MQTT_DeleteClient

Expand Down
13 changes: 12 additions & 1 deletion src/library/mqtt_funcs.c
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,8 @@ static int child_connect(MqttClientHandle *h, struct MsgPort *cmd_port,
#ifdef MIDGE_AMIGA_TLS
if (transport_amissl_connect(transport, &cctx->tls,
(const char *)h->ch_Host, h->ch_Port,
h->ch_Opts.mco_TLSInsecure) != 0)
h->ch_Opts.mco_TLSInsecure,
(const char *)h->ch_Opts.mco_CAFile) != 0)
return MQTTERR_NOTCONNECTED;
/* Extra wakeup source for recv()'s WaitSelect poll, so a
* PUBLISH/SUBSCRIBE/DISCONNECT/QUIT arriving on cmd_port while
Expand Down Expand Up @@ -927,6 +928,11 @@ APTR MQTT_CreateClient(struct Library *_base, STRPTR host, UWORD port,
if (!h->ch_Password)
goto fail_host;
}
if (opts->mco_CAFile) {
h->ch_CAFile = dupstr((const char *)opts->mco_CAFile);
if (!h->ch_CAFile)
goto fail_host;
}
h->ch_Opts.mco_KeepAlive = opts->mco_KeepAlive;
h->ch_Opts.mco_CleanSession = opts->mco_CleanSession;
h->ch_Opts.mco_AutoReconnect = opts->mco_AutoReconnect;
Expand All @@ -936,6 +942,7 @@ APTR MQTT_CreateClient(struct Library *_base, STRPTR host, UWORD port,
h->ch_Opts.mco_ClientID = h->ch_ClientID;
h->ch_Opts.mco_Username = h->ch_Username;
h->ch_Opts.mco_Password = h->ch_Password;
h->ch_Opts.mco_CAFile = h->ch_CAFile;

h->ch_MsgPort = CreateMsgPort();
if (!h->ch_MsgPort)
Expand Down Expand Up @@ -984,6 +991,8 @@ APTR MQTT_CreateClient(struct Library *_base, STRPTR host, UWORD port,
fail_msgport:
DeleteMsgPort(h->ch_MsgPort);
fail_host:
if (h->ch_CAFile)
FreeVec(h->ch_CAFile);
if (h->ch_Password)
FreeVec(h->ch_Password);
if (h->ch_Username)
Expand Down Expand Up @@ -1033,6 +1042,8 @@ VOID MQTT_DeleteClient(struct Library *_base, APTR client)
FreeVec(msg);
DeleteMsgPort(h->ch_MsgPort);

if (h->ch_CAFile)
FreeVec(h->ch_CAFile);
if (h->ch_Password)
FreeVec(h->ch_Password);
if (h->ch_Username)
Expand Down
1 change: 1 addition & 0 deletions src/library/mqtt_priv.h
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ typedef struct MqttClientHandle {
STRPTR ch_ClientID;
STRPTR ch_Username;
STRPTR ch_Password;
STRPTR ch_CAFile;

/* --- QoS 1 publish / SUBACK-wait scratch state -------------------------
* Written by deliver_cb() (called from inside the CHILD subprocess's own
Expand Down
5 changes: 5 additions & 0 deletions src/tools/tool_opts.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ typedef struct {
int tls; /* opt-in, never default-on (see issue #3) */
int tls_insecure; /* skip certificate verification; ignored
without tls */
const char *ca_file; /* extra trust anchor for a private CA (issue
#13); NULL uses only the system/AmiSSL
default trust store. Ignored without tls,
and ignored if tls_insecure is also set
(nothing to verify against then). */
} tool_opts;

/* Connects (assumes `transport` is already connected to the broker),
Expand Down
Loading