Coverage Report

Created: 2026-05-29 06:46

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mosquitto/plugins/dynamic-security/config_init.c
Line
Count
Source
1
/*
2
Copyright (c) 2021 Roger Light <roger@atchoo.org>
3
4
All rights reserved. This program and the accompanying materials
5
are made available under the terms of the Eclipse Public License 2.0
6
and Eclipse Distribution License v1.0 which accompany this distribution.
7
8
The Eclipse Public License is available at
9
   https://www.eclipse.org/legal/epl-2.0/
10
and the Eclipse Distribution License is available at
11
  http://www.eclipse.org/org/documents/edl-v10.php.
12
13
SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
14
15
Contributors:
16
   Roger Light - initial implementation and documentation.
17
*/
18
19
#include "config.h"
20
21
#include <cjson/cJSON.h>
22
#include <ctype.h>
23
#include <errno.h>
24
#include <stdio.h>
25
#include <stdlib.h>
26
#include <string.h>
27
#include <sys/stat.h>
28
#include <openssl/rand.h>
29
30
#include "dynamic_security.h"
31
#include "json_help.h"
32
33
const char pw_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-=_+[]{}@#~,./<>?";
34
35
36
static int add_default_access(cJSON *j_tree)
37
0
{
38
0
  cJSON *j_default_access;
39
40
0
  j_default_access = cJSON_AddObjectToObject(j_tree, "defaultACLAccess");
41
0
  if(j_default_access == NULL){
42
0
    return MOSQ_ERR_NOMEM;
43
0
  }
44
  /* Set default behaviour:
45
   * * Client can not publish to the broker by default.
46
   * * Broker *CAN* publish to the client by default.
47
   * * Client con not subscribe to topics by default.
48
   * * Client *CAN* unsubscribe from topics by default.
49
   */
50
0
  if(cJSON_AddBoolToObject(j_default_access, "publishClientSend", false) == NULL
51
0
      || cJSON_AddBoolToObject(j_default_access, "publishClientReceive", true) == NULL
52
0
      || cJSON_AddBoolToObject(j_default_access, "subscribe", false) == NULL
53
0
      || cJSON_AddBoolToObject(j_default_access, "unsubscribe", true) == NULL
54
0
      ){
55
56
0
    return MOSQ_ERR_NOMEM;
57
0
  }
58
0
  return MOSQ_ERR_SUCCESS;
59
0
}
60
61
62
static int get_password_from_init_file(struct dynsec__data *data, char **pw)
63
0
{
64
0
  FILE *fptr;
65
0
  char buf[1024];
66
0
  int pos;
67
68
0
  if(data->password_init_file == NULL){
69
0
    *pw = NULL;
70
0
    return MOSQ_ERR_SUCCESS;
71
0
  }
72
0
  fptr = mosquitto_fopen(data->password_init_file, "rt", true);
73
0
  if(!fptr){
74
0
    mosquitto_log_printf(MOSQ_LOG_ERR, "Error: Unable to get initial password from '%s', file not accessible.", data->password_init_file);
75
0
    return MOSQ_ERR_INVAL;
76
0
  }
77
0
  if(!fgets(buf, sizeof(buf), fptr)){
78
0
    fclose(fptr);
79
0
    mosquitto_log_printf(MOSQ_LOG_ERR, "Error: Unable to get initial password from '%s', file empty.", data->password_init_file);
80
0
    return MOSQ_ERR_INVAL;
81
0
  }
82
0
  fclose(fptr);
83
84
0
  pos = (int)strlen(buf)-1;
85
0
  while(pos >= 0 && isspace((unsigned char)buf[pos])){
86
0
    buf[pos] = '\0';
87
0
    pos--;
88
0
  }
89
0
  if(strlen(buf) == 0){
90
0
    mosquitto_log_printf(MOSQ_LOG_ERR, "Error: Unable to get initial password from '%s', password is empty.", data->password_init_file);
91
0
    return MOSQ_ERR_INVAL;
92
0
  }
93
0
  *pw = strdup(buf);
94
0
  if(!*pw){
95
0
    mosquitto_log_printf(MOSQ_LOG_ERR, "Error: Unable to get initial password from '%s', out of memory.", data->password_init_file);
96
0
    return MOSQ_ERR_NOMEM;
97
0
  }else{
98
0
    return MOSQ_ERR_SUCCESS;
99
0
  }
100
0
}
101
102
103
/* Generate a password for the admin user
104
 *
105
 * Uses passwords from, in order:
106
 *
107
 * * The password defined in the plugin_opt_password_init_file file
108
 * * The contents of the MOSQUITTO_DYNSEC_PASSWORD environment variable
109
 * * Randomly generated passwords for "admin", "user", stored in plain text at '<plugin_opt_config_file>.pw'
110
 */
111
static int generate_password(struct dynsec__data *data, cJSON *j_client, char **password)
112
0
{
113
0
  struct mosquitto_pw *pw;
114
0
  char *pwenv;
115
116
0
  if(data->init_mode == dpwim_file){
117
0
    if(get_password_from_init_file(data, password)){
118
0
      return MOSQ_ERR_INVAL;
119
0
    }
120
0
  }else if(data->init_mode == dpwim_env){
121
0
    pwenv = getenv("MOSQUITTO_DYNSEC_PASSWORD");
122
0
    if(pwenv == NULL || strlen(pwenv) < 12){
123
0
      mosquitto_log_printf(MOSQ_LOG_ERR, "Error: Not generating dynsec config, MOSQUITTO_DYNSEC_PASSWORD must be at least 12 characters.");
124
0
      return MOSQ_ERR_INVAL;
125
0
    }
126
0
    *password = strdup(pwenv);
127
0
    if(*password == NULL){
128
0
      return MOSQ_ERR_NOMEM;
129
0
    }
130
0
  }else{
131
0
    unsigned char vb;
132
0
    unsigned long v;
133
0
    size_t len;
134
0
    const size_t pwlen = 20;
135
0
    *password = malloc(pwlen+1);
136
0
    if(*password == NULL){
137
0
      return MOSQ_ERR_NOMEM;
138
0
    }
139
0
    len = sizeof(pw_chars)-1;
140
0
    for(size_t i=0; i<pwlen; i++){
141
0
      do{
142
0
        if(RAND_bytes(&vb, 1) != 1){
143
0
          free(*password);
144
0
          return MOSQ_ERR_UNKNOWN;
145
0
        }
146
0
        v = vb;
147
0
      }while(v >= (RAND_MAX - (RAND_MAX % len)));
148
0
      (*password)[i] = pw_chars[v%len];
149
0
    }
150
0
    (*password)[pwlen] = '\0';
151
0
  }
152
153
0
  if(mosquitto_pw_new(&pw, MOSQ_PW_DEFAULT) != MOSQ_ERR_SUCCESS
154
0
      || mosquitto_pw_hash_encoded(pw, *password) != MOSQ_ERR_SUCCESS
155
0
      || cJSON_AddStringToObject(j_client, "encoded_password", mosquitto_pw_get_encoded(pw)) == NULL){
156
157
0
    mosquitto_pw_cleanup(pw);
158
0
    free(*password);
159
0
    *password = NULL;
160
0
    return MOSQ_ERR_UNKNOWN;
161
0
  }
162
163
0
  mosquitto_pw_cleanup(pw);
164
165
0
  return MOSQ_ERR_SUCCESS;
166
0
}
167
168
169
static int client_role_add(cJSON *j_roles, const char *rolename)
170
0
{
171
0
  cJSON *j_role;
172
173
0
  j_role = cJSON_CreateObject();
174
0
  if(j_role == NULL){
175
0
    return MOSQ_ERR_NOMEM;
176
0
  }
177
0
  cJSON_AddItemToArray(j_roles, j_role);
178
0
  if(cJSON_AddStringToObject(j_role, "rolename", rolename) == NULL){
179
0
    return MOSQ_ERR_NOMEM;
180
0
  }else{
181
0
    return MOSQ_ERR_SUCCESS;
182
0
  }
183
0
}
184
185
186
static int client_add_admin(struct dynsec__data *data, FILE *pwfile, cJSON *j_clients)
187
0
{
188
0
  cJSON *j_client, *j_roles;
189
0
  char *password = NULL;
190
191
0
  j_client = cJSON_CreateObject();
192
0
  if(j_client == NULL){
193
0
    return MOSQ_ERR_NOMEM;
194
0
  }
195
0
  if(generate_password(data, j_client, &password)){
196
0
    cJSON_Delete(j_client);
197
0
    return MOSQ_ERR_UNKNOWN;
198
0
  }
199
200
0
  cJSON_AddItemToArray(j_clients, j_client);
201
0
  if(cJSON_AddStringToObject(j_client, "username", "admin") == NULL
202
0
      || cJSON_AddStringToObject(j_client, "textname", "Admin user") == NULL
203
0
      || (j_roles = cJSON_AddArrayToObject(j_client, "roles")) == NULL
204
0
      ){
205
206
0
    cJSON_Delete(j_client);
207
0
    free(password);
208
0
    return MOSQ_ERR_NOMEM;
209
0
  }
210
211
0
  if(client_role_add(j_roles, "super-admin")
212
0
      || client_role_add(j_roles, "sys-observe")
213
0
      || client_role_add(j_roles, "topic-observe")){
214
215
0
    free(password);
216
0
    return MOSQ_ERR_NOMEM;
217
0
  }
218
219
0
  if(data->init_mode == dpwim_random){
220
0
    fprintf(pwfile, "admin %s\n", password);
221
0
  }
222
0
  free(password);
223
224
0
  return MOSQ_ERR_SUCCESS;
225
0
}
226
227
228
static int client_add_user(struct dynsec__data *data, FILE *pwfile, cJSON *j_clients)
229
0
{
230
0
  cJSON *j_client, *j_roles;
231
0
  char *password = NULL;
232
233
0
  if(data->init_mode != dpwim_random){
234
0
    return MOSQ_ERR_SUCCESS;
235
0
  }
236
0
  j_client = cJSON_CreateObject();
237
0
  if(j_client == NULL){
238
0
    return MOSQ_ERR_NOMEM;
239
0
  }
240
241
0
  if(generate_password(data, j_client, &password)){
242
0
    cJSON_Delete(j_client);
243
0
    return MOSQ_ERR_UNKNOWN;
244
0
  }
245
246
0
  if(cJSON_AddStringToObject(j_client, "username", "democlient") == NULL
247
0
      || cJSON_AddStringToObject(j_client, "textname", "Demonstration client with full read/write access to the '#' topic hierarchy.") == NULL
248
0
      || (j_roles = cJSON_AddArrayToObject(j_client, "roles")) == NULL
249
0
      ){
250
251
0
    free(password);
252
0
    cJSON_Delete(j_client);
253
0
    return MOSQ_ERR_NOMEM;
254
0
  }
255
0
  cJSON_AddItemToArray(j_clients, j_client);
256
257
0
  if(client_role_add(j_roles, "client")){
258
0
    free(password);
259
0
    return MOSQ_ERR_NOMEM;
260
0
  }
261
262
0
  fprintf(pwfile, "democlient %s\n", password);
263
0
  free(password);
264
265
0
  return MOSQ_ERR_SUCCESS;
266
0
}
267
268
269
static int add_clients(struct dynsec__data *data, cJSON *j_tree)
270
0
{
271
0
  cJSON *j_clients;
272
0
  char *pwfile;
273
0
  size_t len;
274
0
  FILE *fptr = NULL;
275
276
0
  if(data->init_mode == dpwim_random){
277
0
    len = strlen(data->config_file) + 5;
278
0
    pwfile = malloc(len);
279
0
    if(pwfile == NULL){
280
0
      return MOSQ_ERR_NOMEM;
281
0
    }
282
0
    snprintf(pwfile, len, "%s.pw", data->config_file);
283
0
    fptr = mosquitto_fopen(pwfile, "wb", true);
284
0
    free(pwfile);
285
0
    if(fptr == NULL){
286
0
      return MOSQ_ERR_UNKNOWN;
287
0
    }
288
0
  }
289
290
0
  j_clients = cJSON_AddArrayToObject(j_tree, "clients");
291
0
  if(j_clients == NULL){
292
0
    if(fptr){
293
0
      fclose(fptr);
294
0
    }
295
0
    return MOSQ_ERR_NOMEM;
296
0
  }
297
298
0
  if(client_add_admin(data, fptr, j_clients)
299
0
      || client_add_user(data, fptr, j_clients)
300
0
      ){
301
302
0
    if(fptr){
303
0
      fclose(fptr);
304
0
    }
305
0
    return MOSQ_ERR_NOMEM;
306
0
  }
307
308
0
  if(fptr){
309
0
    fclose(fptr);
310
0
  }
311
0
  return MOSQ_ERR_SUCCESS;
312
0
}
313
314
315
static int group_add_anon(cJSON *j_groups)
316
0
{
317
0
  cJSON *j_group;
318
319
0
  j_group = cJSON_CreateObject();
320
0
  if(j_group == NULL){
321
0
    return MOSQ_ERR_NOMEM;
322
0
  }
323
324
0
  cJSON_AddItemToArray(j_groups, j_group);
325
0
  if(cJSON_AddStringToObject(j_group, "groupname", "unauthenticated") == NULL
326
0
      || cJSON_AddStringToObject(j_group, "textname", "Unauthenticated group") == NULL
327
0
      || cJSON_AddStringToObject(j_group, "textdescription", "If unauthenticated access is allowed, this group can be used to define roles for clients that connect without a password.") == NULL
328
0
      || cJSON_AddArrayToObject(j_group, "roles") == NULL
329
0
      ){
330
331
0
    return MOSQ_ERR_NOMEM;
332
0
  }
333
334
0
  return MOSQ_ERR_SUCCESS;
335
0
}
336
337
338
static int add_groups(cJSON *j_tree)
339
0
{
340
0
  cJSON *j_groups;
341
342
0
  j_groups = cJSON_AddArrayToObject(j_tree, "groups");
343
0
  if(j_groups == NULL){
344
0
    return MOSQ_ERR_NOMEM;
345
0
  }
346
347
0
  return group_add_anon(j_groups);
348
0
}
349
350
351
static int acl_add(cJSON *j_acls, const char *acltype, const char *topic, int priority, bool allow)
352
0
{
353
0
  cJSON *j_acl;
354
355
0
  j_acl = cJSON_CreateObject();
356
0
  cJSON_AddItemToArray(j_acls, j_acl);
357
0
  if(cJSON_AddStringToObject(j_acl, "acltype", acltype) == NULL
358
0
      || cJSON_AddStringToObject(j_acl, "topic", topic) == NULL
359
0
      || cJSON_AddNumberToObject(j_acl, "priority", priority) == NULL
360
0
      || cJSON_AddBoolToObject(j_acl, "allow", allow) == NULL
361
0
      ){
362
0
    return MOSQ_ERR_NOMEM;
363
0
  }else{
364
0
    return MOSQ_ERR_SUCCESS;
365
0
  }
366
0
}
367
368
369
static int add_role_with_full_permission(cJSON *j_roles, const char *role_name, const char *text_description, const char *topic_pattern)
370
0
{
371
0
  cJSON *j_role, *j_acls;
372
373
0
  j_role = cJSON_CreateObject();
374
0
  if(j_role == NULL){
375
0
    return MOSQ_ERR_NOMEM;
376
0
  }
377
0
  cJSON_AddItemToArray(j_roles, j_role);
378
379
0
  if(cJSON_AddStringToObject(j_role, "rolename", role_name) == NULL
380
0
      || cJSON_AddStringToObject(j_role, "textdescription", text_description) == NULL
381
0
      || (j_acls = cJSON_AddArrayToObject(j_role, "acls")) == NULL){
382
0
    return MOSQ_ERR_NOMEM;
383
0
  }
384
385
0
  if(acl_add(j_acls, "publishClientSend", topic_pattern, 0, true)
386
0
      || acl_add(j_acls, "publishClientReceive", topic_pattern, 0, true)
387
0
      || acl_add(j_acls, "subscribePattern", topic_pattern, 0, true)
388
0
      || acl_add(j_acls, "unsubscribePattern", topic_pattern, 0, true)){
389
0
    return MOSQ_ERR_NOMEM;
390
0
  }
391
0
  return MOSQ_ERR_SUCCESS;
392
0
}
393
394
395
static int role_add_sys_notify(cJSON *j_roles)
396
0
{
397
0
  cJSON *j_role, *j_acls;
398
399
0
  j_role = cJSON_CreateObject();
400
0
  if(j_role == NULL){
401
0
    return MOSQ_ERR_NOMEM;
402
0
  }
403
0
  cJSON_AddItemToArray(j_roles, j_role);
404
405
0
  if(cJSON_AddStringToObject(j_role, "rolename", "sys-notify") == NULL
406
0
      || cJSON_AddStringToObject(j_role, "textdescription",
407
0
      "Allow bridges to publish connection state messages.") == NULL
408
0
      || (j_acls = cJSON_AddArrayToObject(j_role, "acls")) == NULL
409
0
      ){
410
411
0
    return MOSQ_ERR_NOMEM;
412
0
  }
413
414
0
  if(acl_add(j_acls, "publishClientSend", "$SYS/broker/connection/%c/state", 0, true)
415
0
      ){
416
417
0
    return MOSQ_ERR_NOMEM;
418
0
  }
419
0
  return MOSQ_ERR_SUCCESS;
420
0
}
421
422
423
static int role_add_sys_observe(cJSON *j_roles)
424
0
{
425
0
  cJSON *j_role, *j_acls;
426
427
0
  j_role = cJSON_CreateObject();
428
0
  if(j_role == NULL){
429
0
    return MOSQ_ERR_NOMEM;
430
0
  }
431
0
  cJSON_AddItemToArray(j_roles, j_role);
432
433
0
  if(cJSON_AddStringToObject(j_role, "rolename", "sys-observe") == NULL
434
0
      || cJSON_AddStringToObject(j_role, "textdescription",
435
0
      "Observe the $SYS topic hierarchy.") == NULL
436
0
      || (j_acls = cJSON_AddArrayToObject(j_role, "acls")) == NULL
437
0
      ){
438
439
0
    return MOSQ_ERR_NOMEM;
440
0
  }
441
442
0
  if(acl_add(j_acls, "publishClientReceive", "$SYS/#", 0, true)
443
0
      || acl_add(j_acls, "subscribePattern", "$SYS/#", 0, true)
444
0
      ){
445
446
0
    return MOSQ_ERR_NOMEM;
447
0
  }
448
0
  return MOSQ_ERR_SUCCESS;
449
0
}
450
451
452
static int role_add_topic_observe(cJSON *j_roles)
453
0
{
454
0
  cJSON *j_role, *j_acls;
455
456
0
  j_role = cJSON_CreateObject();
457
0
  if(j_role == NULL){
458
0
    return MOSQ_ERR_NOMEM;
459
0
  }
460
0
  cJSON_AddItemToArray(j_roles, j_role);
461
462
0
  if(cJSON_AddStringToObject(j_role, "rolename", "topic-observe") == NULL
463
0
      || cJSON_AddStringToObject(j_role, "textdescription",
464
0
      "Read only access to the full application topic hierarchy.") == NULL
465
0
      || (j_acls = cJSON_AddArrayToObject(j_role, "acls")) == NULL
466
0
      ){
467
468
0
    return MOSQ_ERR_NOMEM;
469
0
  }
470
471
0
  if(acl_add(j_acls, "publishClientReceive", "#", 0, true)
472
0
      || acl_add(j_acls, "subscribePattern", "#", 0, true)
473
0
      || acl_add(j_acls, "unsubscribePattern", "#", 0, true)
474
0
      ){
475
476
0
    return MOSQ_ERR_NOMEM;
477
0
  }
478
0
  return MOSQ_ERR_SUCCESS;
479
0
}
480
481
482
static int add_roles(cJSON *j_tree)
483
0
{
484
0
  cJSON *j_roles;
485
486
0
  j_roles = cJSON_AddArrayToObject(j_tree, "roles");
487
0
  if(j_roles == NULL){
488
0
    return MOSQ_ERR_NOMEM;
489
0
  }
490
491
0
  if(add_role_with_full_permission(j_roles, "client", "Read/write access to the full application topic hierarchy.", "#")
492
0
      || add_role_with_full_permission(j_roles, "broker-admin", "Grants access to administer general broker configuration.", "$CONTROL/broker/#")
493
0
      || add_role_with_full_permission(j_roles, "dynsec-admin", "Grants access to administer clients/groups/roles.", "$CONTROL/dynamic-security/#")
494
0
      || add_role_with_full_permission(j_roles, "super-admin", "Grants access to administer all kind of broker controls", "$CONTROL/#")
495
0
      || role_add_sys_notify(j_roles) || role_add_sys_observe(j_roles) || role_add_topic_observe(j_roles)){
496
0
    return MOSQ_ERR_NOMEM;
497
0
  }
498
499
0
  return MOSQ_ERR_SUCCESS;
500
0
}
501
502
503
int dynsec__config_init(struct dynsec__data *data)
504
0
{
505
0
  FILE *fptr;
506
0
  cJSON *j_tree;
507
0
  char *json_str;
508
509
0
  mosquitto_log_printf(MOSQ_LOG_INFO, "Dynamic security plugin config not found, generating a default config.");
510
511
0
  if(data->password_init_file){
512
0
    mosquitto_log_printf(MOSQ_LOG_INFO, "  Using admin password from file '%s'", data->password_init_file);
513
0
    data->init_mode = dpwim_file;
514
0
  }else if(getenv("MOSQUITTO_DYNSEC_PASSWORD")){
515
0
    mosquitto_log_printf(MOSQ_LOG_INFO, "  Using admin password from MOSQUITTO_DYNSEC_PASSWORD environment variable");
516
0
    data->init_mode = dpwim_env;
517
0
  }else{
518
0
    mosquitto_log_printf(MOSQ_LOG_INFO, "  Generated passwords are at %s.pw", data->config_file);
519
0
    data->init_mode = dpwim_random;
520
0
  }
521
522
0
  j_tree = cJSON_CreateObject();
523
0
  if(j_tree == NULL){
524
0
    return MOSQ_ERR_NOMEM;
525
0
  }
526
527
0
  if(add_default_access(j_tree) != MOSQ_ERR_SUCCESS
528
0
      || add_clients(data, j_tree) != MOSQ_ERR_SUCCESS
529
0
      || add_groups(j_tree) != MOSQ_ERR_SUCCESS
530
0
      || add_roles(j_tree) != MOSQ_ERR_SUCCESS
531
0
      || cJSON_AddStringToObject(j_tree, "anonymousGroup", "unauthenticated") == NULL
532
0
      ){
533
534
0
    cJSON_Delete(j_tree);
535
0
    return MOSQ_ERR_NOMEM;
536
0
  }
537
538
0
  json_str = cJSON_Print(j_tree);
539
0
  cJSON_Delete(j_tree);
540
0
  if(json_str == NULL){
541
0
    return MOSQ_ERR_NOMEM;
542
0
  }
543
544
0
  fptr = mosquitto_fopen(data->config_file, "wb", true);
545
0
  if(fptr == NULL){
546
0
    return MOSQ_ERR_UNKNOWN;
547
0
  }
548
0
  fprintf(fptr, "%s", json_str);
549
0
  free(json_str);
550
0
  fclose(fptr);
551
552
0
  return MOSQ_ERR_SUCCESS;
553
0
}