1 /*
2 * Copyright 2024-2026 the Pacemaker project contributors
3 *
4 * The version control history for this file may have further details.
5 *
6 * This source code is licensed under the GNU Lesser General Public License
7 * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
8 */
9
10 #include <crm_internal.h>
11
12 #include <ctype.h> // isxdigit
13 #include <errno.h> // EAGAIN, ENODATA, EPROTO, EINVAL, ETIME
14 #include <pwd.h> // getpwuid
15 #include <signal.h> // signal, SIGPIPE, SIG_IGN
16 #include <stdbool.h> // bool
17 #include <stdlib.h> // NULL, getenv, free
18 #include <string.h> // memcpy, strdup, strlen
19 #include <sys/stat.h> // S_ISREG, stat
20 #include <syslog.h> // LOG_WARNING
21 #include <time.h> // time, time_t
22 #include <unistd.h> // geteuid
23
24 #include <glib.h> // g_clear_pointer, g_file_get_contents
25 #include <gnutls/gnutls.h> // gnutls_*, GNUTLS_E_SUCCESS
26 #include <gnutls/x509.h> // gnutls_x509_*
27 #include <qb/qblog.h> // QB_XS
28
29 #include <crm/common/internal.h>
30 #include <crm/common/iso8601.h> // crm_time_free, crm_time_log_date
31 #include <crm/common/logging.h> // CRM_CHECK
32 #include <crm/common/results.h> // pcmk_rc_*
33
34 static char *
35 get_gnutls_priorities(gnutls_credentials_type_t cred_type)
36 {
37 const char *prio_base = pcmk__env_option(PCMK__ENV_TLS_PRIORITIES);
38
39 if (prio_base == NULL) {
40 prio_base = PCMK__GNUTLS_PRIORITIES;
41 }
42
43 if (cred_type == GNUTLS_CRD_ANON) {
44 return pcmk__assert_asprintf("%s:+ANON-DH", prio_base);
45 } else if (cred_type == GNUTLS_CRD_PSK) {
46 return pcmk__assert_asprintf("%s:+DHE-PSK:+PSK", prio_base);
47 } else {
48 return strdup(prio_base);
49 }
50 }
51
52 static const char *
53 tls_cred_str(gnutls_credentials_type_t cred_type)
54 {
55 if (cred_type == GNUTLS_CRD_ANON) {
56 return "unauthenticated";
57 } else if (cred_type == GNUTLS_CRD_PSK) {
58 return "shared-key-authenticated";
59 } else if (cred_type == GNUTLS_CRD_CERTIFICATE) {
60 return "certificate-authenticated";
61 } else {
62 return "unknown";
63 }
64 }
65
66 static int
67 tls_load_x509_data(pcmk__tls_t *tls)
68 {
69 int rc;
70
71 CRM_CHECK(tls->cred_type == GNUTLS_CRD_CERTIFICATE, return EINVAL);
72
73 /* Load a trusted CA to be used to verify client certificates. Use
74 * of this function instead of gnutls_certificate_set_x509_system_trust
75 * means we do not look at the system-wide authorities installed in
76 * /etc/pki somewhere. This requires the cluster admin to set up their
77 * own CA.
78 */
79 rc = gnutls_certificate_set_x509_trust_file(tls->credentials.cert,
80 tls->ca_file,
81 GNUTLS_X509_FMT_PEM);
82 if (rc <= 0) {
83 pcmk__err("Failed to set X509 CA file: %s", gnutls_strerror(rc));
84 return ENODATA;
85 }
86
87 /* If a Certificate Revocation List (CRL) file was given in the environment,
88 * load that now so we know which clients have been banned.
89 */
90 if (tls->crl_file != NULL) {
91 rc = gnutls_certificate_set_x509_crl_file(tls->credentials.cert,
92 tls->crl_file,
93 GNUTLS_X509_FMT_PEM);
94 if (rc < 0) {
95 pcmk__err("Failed to set X509 CRL file: %s", gnutls_strerror(rc));
96 return ENODATA;
97 }
98 }
99
100 /* NULL = no password for the key, GNUTLS_PKCS_PLAIN = unencrypted key
101 * file
102 */
103 rc = gnutls_certificate_set_x509_key_file2(tls->credentials.cert,
104 tls->cert_file, tls->key_file,
105 GNUTLS_X509_FMT_PEM, NULL,
106 GNUTLS_PKCS_PLAIN);
107 if (rc < 0) {
108 pcmk__err("Failed to set X509 cert/key pair: %s", gnutls_strerror(rc));
109 return ENODATA;
110 }
111
112 return pcmk_rc_ok;
113 }
114
115 static void
116 _gnutls_log_func(int level, const char *msg)
117 {
118 pcmk__trace("%s", msg);
119 }
120
121 void
122 pcmk__free_tls(pcmk__tls_t *tls)
123 {
124 if (tls == NULL) {
125 return;
126 }
127
128 /* This is only set on the server side. */
129 if (tls->server) {
130 gnutls_dh_params_deinit(tls->dh_params);
131 }
132
133 if (tls->cred_type == GNUTLS_CRD_ANON) {
134 if (tls->server) {
135 gnutls_anon_free_server_credentials(tls->credentials.anon_s);
136 } else {
137 gnutls_anon_free_client_credentials(tls->credentials.anon_c);
138 }
139 } else if (tls->cred_type == GNUTLS_CRD_CERTIFICATE) {
140 gnutls_certificate_free_credentials(tls->credentials.cert);
141 } else if (tls->cred_type == GNUTLS_CRD_PSK) {
142 if (tls->server) {
143 gnutls_psk_free_server_credentials(tls->credentials.psk_s);
144 } else {
145 gnutls_psk_free_client_credentials(tls->credentials.psk_c);
146 }
147 }
148
149 free(tls);
150 }
151
152 int
153 pcmk__init_tls(pcmk__tls_t **tls, bool server, bool have_psk)
154 {
155 int rc = pcmk_rc_ok;
156
|
(1) Event path: |
Condition "*tls != NULL", taking false branch. |
157 if (*tls != NULL) {
158 return rc;
159 }
160
161 *tls = pcmk__assert_alloc(1, sizeof(pcmk__tls_t));
162
163 signal(SIGPIPE, SIG_IGN);
164
165 gnutls_global_set_log_level(8);
166 gnutls_global_set_log_function(_gnutls_log_func);
167
|
(2) Event path: |
Condition "server", taking true branch. |
168 if (server) {
169 rc = pcmk__init_tls_dh(&(*tls)->dh_params);
|
(3) Event path: |
Condition "rc != pcmk_rc_ok", taking false branch. |
170 if (rc != pcmk_rc_ok) {
171 g_clear_pointer(tls, pcmk__free_tls);
172 return rc;
173 }
174 }
175
|
(4) Event path: |
Condition "pcmk__x509_enabled()", taking true branch. |
176 if (pcmk__x509_enabled()) {
177 (*tls)->cred_type = GNUTLS_CRD_CERTIFICATE;
178
|
(5) Event path: |
Falling through to end of if statement. |
179 } else if (have_psk) {
180 (*tls)->cred_type = GNUTLS_CRD_PSK;
181
182 } else {
183 (*tls)->cred_type = GNUTLS_CRD_ANON;
184 }
185
186 (*tls)->server = server;
187
|
(6) Event path: |
Condition "(*tls)->cred_type == GNUTLS_CRD_ANON", taking false branch. |
188 if ((*tls)->cred_type == GNUTLS_CRD_ANON) {
189 pcmk__warn("Using anonymous authentication. This is insecure and will "
190 "be removed in a future release. Use X509 certificates or PSK "
191 "instead.");
192
193 if (server) {
194 gnutls_anon_allocate_server_credentials(&(*tls)->credentials.anon_s);
195 gnutls_anon_set_server_dh_params((*tls)->credentials.anon_s,
196 (*tls)->dh_params);
197 } else {
198 gnutls_anon_allocate_client_credentials(&(*tls)->credentials.anon_c);
199 }
200
|
(7) Event path: |
Condition "(*tls)->cred_type == GNUTLS_CRD_CERTIFICATE", taking true branch. |
201 } else if ((*tls)->cred_type == GNUTLS_CRD_CERTIFICATE) {
202 /* Try the PCMK_ version of each environment variable first, and if
203 * it's not set then try the CIB_ version.
204 */
205 (*tls)->ca_file = pcmk__env_option(PCMK__ENV_CA_FILE);
|
(8) Event path: |
Condition "pcmk__str_empty((*tls)->ca_file)", taking true branch. |
206 if (pcmk__str_empty((*tls)->ca_file)) {
207 (*tls)->ca_file = getenv("CIB_ca_file");
208 }
209
210 (*tls)->cert_file = pcmk__env_option(PCMK__ENV_CERT_FILE);
|
(9) Event path: |
Condition "pcmk__str_empty((*tls)->cert_file)", taking true branch. |
211 if (pcmk__str_empty((*tls)->cert_file)) {
212 (*tls)->cert_file = getenv("CIB_cert_file");
213 }
214
215 (*tls)->crl_file = pcmk__env_option(PCMK__ENV_CRL_FILE);
|
(10) Event path: |
Condition "pcmk__str_empty((*tls)->crl_file)", taking true branch. |
216 if (pcmk__str_empty((*tls)->crl_file)) {
217 (*tls)->crl_file = getenv("CIB_crl_file");
218 }
219
220 (*tls)->key_file = pcmk__env_option(PCMK__ENV_KEY_FILE);
|
(11) Event path: |
Condition "pcmk__str_empty((*tls)->key_file)", taking true branch. |
221 if (pcmk__str_empty((*tls)->key_file)) {
222 (*tls)->key_file = getenv("CIB_key_file");
223 }
224
225 gnutls_certificate_allocate_credentials(&(*tls)->credentials.cert);
226
|
(12) Event path: |
Condition "server", taking true branch. |
227 if (server) {
228 gnutls_certificate_set_dh_params((*tls)->credentials.cert,
229 (*tls)->dh_params);
230
231 }
232
233 rc = tls_load_x509_data(*tls);
|
(13) Event path: |
Condition "rc != pcmk_rc_ok", taking true branch. |
234 if (rc != pcmk_rc_ok) {
|
CID (unavailable; MK=81ade510660048f65bd5a8ddb85e76b1) (#2 of 2): Inconsistent C union access (INCONSISTENT_UNION_ACCESS): |
|
(14) Event assign_union_field: |
The union field "in" of "_pp" is written. |
|
(15) Event inconsistent_union_field_access: |
In "_pp.out", the union field used: "out" is inconsistent with the field most recently stored: "in". |
235 g_clear_pointer(tls, pcmk__free_tls);
236 return rc;
237 }
238 } else { // GNUTLS_CRD_PSK
239 if (server) {
240 gnutls_psk_allocate_server_credentials(&(*tls)->credentials.psk_s);
241 gnutls_psk_set_server_dh_params((*tls)->credentials.psk_s,
242 (*tls)->dh_params);
243 } else {
244 gnutls_psk_allocate_client_credentials(&(*tls)->credentials.psk_c);
245 }
246 }
247
248 return rc;
249 }
250
251 int
252 pcmk__init_tls_dh(gnutls_dh_params_t *dh_params)
253 {
254 int rc = GNUTLS_E_SUCCESS;
255 unsigned int dh_bits = 0;
256 int dh_max_bits = 0;
257
258 rc = gnutls_dh_params_init(dh_params);
259 if (rc != GNUTLS_E_SUCCESS) {
260 goto error;
261 }
262
263 dh_bits = gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH,
264 GNUTLS_SEC_PARAM_NORMAL);
265 if (dh_bits == 0) {
266 rc = GNUTLS_E_DH_PRIME_UNACCEPTABLE;
267 goto error;
268 }
269
270 pcmk__scan_min_int(pcmk__env_option(PCMK__ENV_DH_MAX_BITS), &dh_max_bits, 0);
271 if ((dh_max_bits > 0) && (dh_bits > dh_max_bits)) {
272 dh_bits = dh_max_bits;
273 }
274
275 pcmk__info("Generating Diffie-Hellman parameters with %u-bit prime for TLS",
276 dh_bits);
277 rc = gnutls_dh_params_generate2(*dh_params, dh_bits);
278 if (rc != GNUTLS_E_SUCCESS) {
279 goto error;
280 }
281
282 return pcmk_rc_ok;
283
284 error:
285 pcmk__err("Could not initialize Diffie-Hellman parameters for TLS: %s "
286 QB_XS " rc=%d",
287 gnutls_strerror(rc), rc);
288 return EPROTO;
289 }
290
291 gnutls_session_t
292 pcmk__new_tls_session(pcmk__tls_t *tls, int csock)
293 {
294 unsigned int conn_type = GNUTLS_CLIENT;
295 int rc = GNUTLS_E_SUCCESS;
296 char *prio = NULL;
297 gnutls_session_t session = NULL;
298
299 CRM_CHECK((tls != NULL) && (csock >= 0), return NULL);
300
301 if (tls->server) {
302 conn_type = GNUTLS_SERVER;
303 }
304
305 rc = gnutls_init(&session, conn_type);
306 if (rc != GNUTLS_E_SUCCESS) {
307 goto error;
308 }
309
310 /* Determine list of acceptable ciphers, etc. Pacemaker always adds the
311 * values required for its functionality.
312 *
313 * For an example of anonymous authentication, see:
314 * http://www.manpagez.com/info/gnutls/gnutls-2.10.4/gnutls_81.php#Echo-Server-with-anonymous-authentication
315 */
316 prio = get_gnutls_priorities(tls->cred_type);
317
318 /* @TODO On the server side, it would be more efficient to cache the
319 * priority with gnutls_priority_init2() and set it with
320 * gnutls_priority_set() for all sessions.
321 */
322 rc = gnutls_priority_set_direct(session, prio, NULL);
323 if (rc != GNUTLS_E_SUCCESS) {
324 goto error;
325 }
326
327 gnutls_transport_set_int(session, csock);
328
329 /* gnutls does not make this easy */
330 if (tls->cred_type == GNUTLS_CRD_ANON && tls->server) {
331 rc = gnutls_credentials_set(session, tls->cred_type, tls->credentials.anon_s);
332 } else if (tls->cred_type == GNUTLS_CRD_ANON) {
333 rc = gnutls_credentials_set(session, tls->cred_type, tls->credentials.anon_c);
334 } else if (tls->cred_type == GNUTLS_CRD_CERTIFICATE) {
335 rc = gnutls_credentials_set(session, tls->cred_type, tls->credentials.cert);
336 } else if (tls->cred_type == GNUTLS_CRD_PSK && tls->server) {
337 rc = gnutls_credentials_set(session, tls->cred_type, tls->credentials.psk_s);
338 } else if (tls->cred_type == GNUTLS_CRD_PSK) {
339 rc = gnutls_credentials_set(session, tls->cred_type, tls->credentials.psk_c);
340 } else {
341 pcmk__err("Unknown credential type: %d", tls->cred_type);
342 rc = EINVAL;
343 goto error;
344 }
345
346 if (rc != GNUTLS_E_SUCCESS) {
347 goto error;
348 }
349
350 free(prio);
351
352 if (tls->cred_type == GNUTLS_CRD_CERTIFICATE) {
353 if (conn_type == GNUTLS_SERVER) {
354 /* Require the client to send a certificate for the server to verify. */
355 gnutls_certificate_server_set_request(session, GNUTLS_CERT_REQUIRE);
356 }
357
358 // Register a function to verify the peer's certificate
359 gnutls_session_set_verify_cert(session, NULL, 0);
360 }
361
362 return session;
363
364 error:
365 pcmk__err("Could not initialize %s TLS %s session: %s "
366 QB_XS " rc=%d priority='%s'",
367 tls_cred_str(tls->cred_type),
368 ((conn_type == GNUTLS_SERVER)? "server" : "client"),
369 gnutls_strerror(rc), rc, prio);
370 free(prio);
371 if (session != NULL) {
372 gnutls_deinit(session);
373 }
374 return NULL;
375 }
376
377 int
378 pcmk__tls_get_client_sock(const pcmk__remote_t *remote)
379 {
380 pcmk__assert((remote != NULL) && (remote->tls_session != NULL));
381
382 return gnutls_transport_get_int(remote->tls_session);
383 }
384
385 int
386 pcmk__read_handshake_data(const pcmk__client_t *client)
387 {
388 int rc = 0;
389
390 pcmk__assert((client != NULL) && (client->remote != NULL)
391 && (client->remote->tls_session != NULL));
392
393 do {
394 rc = gnutls_handshake(client->remote->tls_session);
395 } while (rc == GNUTLS_E_INTERRUPTED);
396
397 if (rc == GNUTLS_E_AGAIN) {
398 /* No more data is available at the moment. This function should be
399 * invoked again once the client sends more.
400 */
401 return EAGAIN;
402 } else if (rc == GNUTLS_E_CERTIFICATE_VERIFICATION_ERROR) {
403 int type = gnutls_certificate_type_get(client->remote->tls_session);
404 unsigned int status = gnutls_session_get_verify_cert_status(client->remote->tls_session);
405 gnutls_datum_t out;
406
407 gnutls_certificate_verification_status_print(status, type, &out, 0);
408 pcmk__err("Certificate verification failed: %s", out.data);
409 gnutls_free(out.data);
410 } else if (rc != GNUTLS_E_SUCCESS) {
411 pcmk__err("TLS handshake with remote client failed: %s " QB_XS " rc=%d",
412 gnutls_strerror(rc), rc);
413 return EPROTO;
414 }
415 return pcmk_rc_ok;
416 }
417
418 void
419 pcmk__tls_client_add_psk_key(pcmk__tls_t *tls, const char *username,
420 gnutls_datum_t *key, bool raw)
421 {
422 gnutls_psk_set_client_credentials(tls->credentials.psk_c, username, key,
423 raw ? GNUTLS_PSK_KEY_RAW : GNUTLS_PSK_KEY_HEX);
424 }
425
426 void
427 pcmk__tls_check_cert_expiration(gnutls_session_t session)
428 {
429 gnutls_x509_crt_t cert;
430 const gnutls_datum_t *datum = NULL;
431 time_t expiry;
432
433 if (session == NULL) {
434 return;
435 }
436
437 if (gnutls_certificate_type_get(session) != GNUTLS_CRT_X509) {
438 return;
439 }
440
441 datum = gnutls_certificate_get_ours(session);
442 if (datum == NULL) {
443 return;
444 }
445
446 gnutls_x509_crt_init(&cert);
447 gnutls_x509_crt_import(cert, datum, GNUTLS_X509_FMT_DER);
448
449 expiry = gnutls_x509_crt_get_expiration_time(cert);
450
451 if (expiry != -1) {
452 time_t now = time(NULL);
453
454 /* If the cert is going to expire within ~ one month (30 days), log it */
455 if (expiry - now <= 60 * 60 * 24 * 30) {
456 crm_time_t *expiry_t = pcmk__copy_timet(expiry);
457
458 pcmk__time_log(LOG_WARNING, "TLS certificate will expire on",
459 expiry_t, crm_time_log_date|crm_time_log_timeofday);
460 crm_time_free(expiry_t);
461 }
462 }
463
464 gnutls_x509_crt_deinit(cert);
465 }
466
467 int
468 pcmk__tls_client_try_handshake(pcmk__remote_t *remote, int *gnutls_rc)
469 {
470 int rc = pcmk_rc_ok;
471
472 pcmk__assert(gnutls_rc != NULL);
473 *gnutls_rc = GNUTLS_E_SUCCESS;
474
475 rc = gnutls_handshake(remote->tls_session);
476
477 switch (rc) {
478 case GNUTLS_E_SUCCESS:
479 rc = pcmk_rc_ok;
480 break;
481
482 case GNUTLS_E_INTERRUPTED:
483 case GNUTLS_E_AGAIN:
484 rc = EAGAIN;
485 break;
486
487 case GNUTLS_E_CERTIFICATE_VERIFICATION_ERROR: {
488 int type = gnutls_certificate_type_get(remote->tls_session);
489 unsigned int status = gnutls_session_get_verify_cert_status(remote->tls_session);
490 gnutls_datum_t out;
491
492 gnutls_certificate_verification_status_print(status, type, &out, 0);
493 pcmk__err("Certificate verification failed: %s", out.data);
494 gnutls_free(out.data);
495
496 *gnutls_rc = rc;
497 rc = EPROTO;
498 break;
499 }
500
501 default:
502 *gnutls_rc = rc;
503 rc = EPROTO;
504 break;
505 }
506
507 return rc;
508 }
509
510 int
511 pcmk__tls_client_handshake(pcmk__remote_t *remote, int timeout_sec,
512 int *gnutls_rc)
513 {
514 const time_t time_limit = time(NULL) + timeout_sec;
515
516 do {
517 int rc = pcmk__tls_client_try_handshake(remote, gnutls_rc);
518
519 if (rc != EAGAIN) {
520 return rc;
521 }
522 } while (time(NULL) < time_limit);
523
524 return ETIME;
525 }
526
527 bool
528 pcmk__x509_enabled(void)
529 {
530 /* Environment variables for servers come through the sysconfig file, and
531 * have names like PCMK_<whatever>. Environment variables for clients come
532 * from the environment and have names like CIB_<whatever>. This function
533 * is used for both, so we need to check both.
534 */
535 return (!pcmk__str_empty(pcmk__env_option(PCMK__ENV_CERT_FILE)) ||
536 !pcmk__str_empty(getenv("CIB_cert_file"))) &&
537 (!pcmk__str_empty(pcmk__env_option(PCMK__ENV_CA_FILE)) ||
538 !pcmk__str_empty(getenv("CIB_ca_file"))) &&
539 (!pcmk__str_empty(pcmk__env_option(PCMK__ENV_KEY_FILE)) ||
540 !pcmk__str_empty(getenv("CIB_key_file")));
541 }
542
543 void
544 pcmk__copy_key(gnutls_datum_t *dest, const gnutls_datum_t *source)
545 {
546 pcmk__assert((dest != NULL) && (source != NULL) && (source->data != NULL));
547
548 dest->data = gnutls_malloc(source->size);
549 pcmk__mem_assert(dest->data);
550
551 memcpy(dest->data, source->data, source->size);
552 dest->size = source->size;
553 }
554
555 int
556 pcmk__load_key(const char *location, gnutls_datum_t *key, bool raw)
557 {
558 gchar *contents = NULL;
559 gsize len = 0;
560 int rc = pcmk_rc_ok;
561
562 pcmk__assert((location != NULL) && (key != NULL));
563
564 if (!g_file_get_contents(location, &contents, &len, NULL)) {
565 return ENOKEY;
566 }
567
568 if (!raw) {
569 /* This is a plain text key. gnutls expects only hex characters, so
570 * remove any whitespace and check that the characters are valid.
571 */
572 contents = g_strstrip(contents);
573
574 for (const char *c = contents; *c != '\0'; c++) {
575 if (!isxdigit(*c)) {
576 pcmk__err("Key file %s contains characters besides hex digits",
577 location);
578 rc = ENOKEY;
579 goto done;
580 }
581 }
582
583 len = strlen(contents);
584 }
585
586 key->size = len;
587 key->data = gnutls_malloc(key->size);
588 pcmk__mem_assert(key->data);
589 memcpy(key->data, contents, key->size);
590
591 done:
592 g_free(contents);
593 return rc;
594 }
595
596 bool
597 pcmk__cred_file_useable(const char *location, bool *file_exists)
598 {
599 int rc = pcmk_rc_ok;
600 struct stat sb;
601 uid_t euid;
602
603 pcmk__assert((location != NULL) && (file_exists != NULL));
604
605 rc = stat(location, &sb);
606 if ((rc != 0) || !S_ISREG(sb.st_mode)) {
607 *file_exists = false;
608 return false;
609 }
610
611 *file_exists = true;
612
613 euid = geteuid();
614 if (sb.st_uid != euid) {
615 struct passwd *pwent = getpwuid(euid);
616
617 if (pwent == NULL) {
618 pcmk__err("Refusing to use PSK credentials file %s because it is "
619 "not owned by UID %lld", (long long) euid);
620 } else {
621 pcmk__err("Refusing to use PSK credentials file %s because it is "
622 "not owned by %s", location, pwent->pw_name);
623 }
624
625 return false;
626 }
627
628 if ((sb.st_mode & (S_IRWXG | S_IRWXO)) != 0) {
629 pcmk__err("Refusing to use PSK credentials file %s because it has "
630 "group and/or other permissions set", location);
631 return false;
632 }
633
634 return true;
635 }
636