Coverage Report

Created: 2024-10-12 00:30

/src/pacemaker/lib/common/digest.c
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * Copyright 2015-2024 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 <stdbool.h>
13
#include <stdio.h>
14
#include <unistd.h>
15
#include <string.h>
16
#include <stdlib.h>
17
18
#include <glib.h>               // GString, etc.
19
#include <gnutls/crypto.h>      // gnutls_hash_fast(), gnutls_hash_get_len()
20
#include <gnutls/gnutls.h>      // gnutls_strerror()
21
22
#include <crm/crm.h>
23
#include <crm/common/xml.h>
24
#include "crmcommon_private.h"
25
26
#define BEST_EFFORT_STATUS 0
27
28
/*!
29
 * \internal
30
 * \brief Dump XML in a format used with v1 digests
31
 *
32
 * \param[in] xml  Root of XML to dump
33
 *
34
 * \return Newly allocated buffer containing dumped XML
35
 */
36
static GString *
37
dump_xml_for_digest(xmlNodePtr xml)
38
0
{
39
0
    GString *buffer = g_string_sized_new(1024);
40
41
    /* for compatibility with the old result which is used for v1 digests */
42
0
    g_string_append_c(buffer, ' ');
43
0
    pcmk__xml_string(xml, 0, buffer, 0);
44
0
    g_string_append_c(buffer, '\n');
45
46
0
    return buffer;
47
0
}
48
49
/*!
50
 * \internal
51
 * \brief Calculate and return v1 digest of XML tree
52
 *
53
 * \param[in] input  Root of XML to digest
54
 *
55
 * \return Newly allocated string containing digest
56
 *
57
 * \note Example return value: "c048eae664dba840e1d2060f00299e9d"
58
 */
59
static char *
60
calculate_xml_digest_v1(xmlNode *input)
61
0
{
62
0
    GString *buffer = dump_xml_for_digest(input);
63
0
    char *digest = NULL;
64
65
    // buffer->len > 2 for initial space and trailing newline
66
0
    CRM_CHECK(buffer->len > 2,
67
0
              g_string_free(buffer, TRUE);
68
0
              return NULL);
69
70
0
    digest = crm_md5sum((const char *) buffer->str);
71
0
    crm_log_xml_trace(input, "digest:source");
72
73
0
    g_string_free(buffer, TRUE);
74
0
    return digest;
75
0
}
76
77
/*!
78
 * \internal
79
 * \brief Calculate and return the digest of a CIB, suitable for storing on disk
80
 *
81
 * \param[in] input  Root of XML to digest
82
 *
83
 * \return Newly allocated string containing digest
84
 */
85
char *
86
pcmk__digest_on_disk_cib(xmlNode *input)
87
0
{
88
    /* Always use the v1 format for on-disk digests.
89
     * * Switching to v2 affects even full-restart upgrades, so it would be a
90
     *   compatibility nightmare.
91
     * * We only use this once at startup. All other invocations are in a
92
     *   separate child process.
93
     */
94
0
    return calculate_xml_digest_v1(input);
95
0
}
96
97
/*!
98
 * \internal
99
 * \brief Calculate and return digest of an operation XML element
100
 *
101
 * The digest is invariant to changes in the order of XML attributes, provided
102
 * that \p input has no children.
103
 *
104
 * \param[in] input  Root of XML to digest
105
 *
106
 * \return Newly allocated string containing digest
107
 */
108
char *
109
pcmk__digest_operation(xmlNode *input)
110
0
{
111
    /* Switching to v2 digests would likely cause restarts during rolling
112
     * upgrades.
113
     *
114
     * @TODO Confirm this. Switch to v2 if safe, or drop this TODO otherwise.
115
     */
116
0
    xmlNode *sorted = pcmk__xml_copy(NULL, input);
117
0
    char *digest = NULL;
118
119
0
    pcmk__xe_sort_attrs(sorted);
120
0
    digest = calculate_xml_digest_v1(sorted);
121
122
0
    pcmk__xml_free(sorted);
123
0
    return digest;
124
0
}
125
126
/*!
127
 * \internal
128
 * \brief Calculate and return the digest of an XML tree
129
 *
130
 * \param[in] xml     XML tree to digest
131
 * \param[in] filter  Whether to filter certain XML attributes
132
 *
133
 * \return Newly allocated string containing digest
134
 */
135
char *
136
pcmk__digest_xml(xmlNode *xml, bool filter)
137
0
{
138
    /* @TODO Filtering accounts for significant CPU usage. Consider removing if
139
     * possible.
140
     */
141
0
    char *digest = NULL;
142
0
    GString *buf = g_string_sized_new(1024);
143
144
0
    pcmk__xml_string(xml, (filter? pcmk__xml_fmt_filtered : 0), buf, 0);
145
0
    digest = crm_md5sum(buf->str);
146
147
0
    pcmk__if_tracing(
148
0
        {
149
0
            char *trace_file = crm_strdup_printf("%s/digest-%s",
150
0
                                                 pcmk__get_tmpdir(), digest);
151
152
0
            crm_trace("Saving %s.%s.%s to %s",
153
0
                      crm_element_value(xml, PCMK_XA_ADMIN_EPOCH),
154
0
                      crm_element_value(xml, PCMK_XA_EPOCH),
155
0
                      crm_element_value(xml, PCMK_XA_NUM_UPDATES),
156
0
                      trace_file);
157
0
            save_xml_to_file(xml, "digest input", trace_file);
158
0
            free(trace_file);
159
0
        },
160
0
        {}
161
0
    );
162
0
    g_string_free(buf, TRUE);
163
0
    return digest;
164
0
}
165
166
/*!
167
 * \internal
168
 * \brief Check whether calculated digest of given XML matches expected digest
169
 *
170
 * \param[in] input     Root of XML tree to digest
171
 * \param[in] expected  Expected digest in on-disk format
172
 *
173
 * \return true if digests match, false on mismatch or error
174
 */
175
bool
176
pcmk__verify_digest(xmlNode *input, const char *expected)
177
0
{
178
0
    char *calculated = NULL;
179
0
    bool passed;
180
181
0
    if (input != NULL) {
182
0
        calculated = pcmk__digest_on_disk_cib(input);
183
0
        if (calculated == NULL) {
184
0
            crm_perror(LOG_ERR, "Could not calculate digest for comparison");
185
0
            return false;
186
0
        }
187
0
    }
188
0
    passed = pcmk__str_eq(expected, calculated, pcmk__str_casei);
189
0
    if (passed) {
190
0
        crm_trace("Digest comparison passed: %s", calculated);
191
0
    } else {
192
0
        crm_err("Digest comparison failed: expected %s, calculated %s",
193
0
                expected, calculated);
194
0
    }
195
0
    free(calculated);
196
0
    return passed;
197
0
}
198
199
/*!
200
 * \internal
201
 * \brief Check whether an XML attribute should be excluded from CIB digests
202
 *
203
 * \param[in] name  XML attribute name
204
 *
205
 * \return true if XML attribute should be excluded from CIB digest calculation
206
 */
207
bool
208
pcmk__xa_filterable(const char *name)
209
0
{
210
0
    static const char *filter[] = {
211
0
        PCMK_XA_CRM_DEBUG_ORIGIN,
212
0
        PCMK_XA_CIB_LAST_WRITTEN,
213
0
        PCMK_XA_UPDATE_ORIGIN,
214
0
        PCMK_XA_UPDATE_CLIENT,
215
0
        PCMK_XA_UPDATE_USER,
216
0
    };
217
218
0
    for (int i = 0; i < PCMK__NELEM(filter); i++) {
219
0
        if (strcmp(name, filter[i]) == 0) {
220
0
            return true;
221
0
        }
222
0
    }
223
0
    return false;
224
0
}
225
226
char *
227
crm_md5sum(const char *buffer)
228
0
{
229
0
    char *digest = NULL;
230
0
    gchar *raw_digest = NULL;
231
232
0
    if (buffer == NULL) {
233
0
        return NULL;
234
0
    }
235
236
0
    raw_digest = g_compute_checksum_for_string(G_CHECKSUM_MD5, buffer, -1);
237
238
0
    if (raw_digest == NULL) {
239
0
        crm_err("Failed to calculate hash");
240
0
        return NULL;
241
0
    }
242
243
0
    digest = pcmk__str_copy(raw_digest);
244
0
    g_free(raw_digest);
245
246
0
    crm_trace("Digest %s.", digest);
247
0
    return digest;
248
0
}
249
250
// Return true if a is an attribute that should be filtered
251
static bool
252
should_filter_for_digest(xmlAttrPtr a, void *user_data)
253
0
{
254
0
    if (strncmp((const char *) a->name, CRM_META "_",
255
0
                sizeof(CRM_META " ") - 1) == 0) {
256
0
        return true;
257
0
    }
258
0
    return pcmk__str_any_of((const char *) a->name,
259
0
                            PCMK_XA_ID,
260
0
                            PCMK_XA_CRM_FEATURE_SET,
261
0
                            PCMK__XA_OP_DIGEST,
262
0
                            PCMK__META_ON_NODE,
263
0
                            PCMK__META_ON_NODE_UUID,
264
0
                            "pcmk_external_ip",
265
0
                            NULL);
266
0
}
267
268
/*!
269
 * \internal
270
 * \brief Remove XML attributes not needed for operation digest
271
 *
272
 * \param[in,out] param_set  XML with operation parameters
273
 */
274
void
275
pcmk__filter_op_for_digest(xmlNode *param_set)
276
0
{
277
0
    char *key = NULL;
278
0
    char *timeout = NULL;
279
0
    guint interval_ms = 0;
280
281
0
    if (param_set == NULL) {
282
0
        return;
283
0
    }
284
285
    /* Timeout is useful for recurring operation digests, so grab it before
286
     * removing meta-attributes
287
     */
288
0
    key = crm_meta_name(PCMK_META_INTERVAL);
289
0
    if (crm_element_value_ms(param_set, key, &interval_ms) != pcmk_ok) {
290
0
        interval_ms = 0;
291
0
    }
292
0
    free(key);
293
0
    key = NULL;
294
0
    if (interval_ms != 0) {
295
0
        key = crm_meta_name(PCMK_META_TIMEOUT);
296
0
        timeout = crm_element_value_copy(param_set, key);
297
0
    }
298
299
    // Remove all CRM_meta_* attributes and certain other attributes
300
0
    pcmk__xe_remove_matching_attrs(param_set, should_filter_for_digest, NULL);
301
302
    // Add timeout back for recurring operation digests
303
0
    if (timeout != NULL) {
304
0
        crm_xml_add(param_set, key, timeout);
305
0
    }
306
0
    free(timeout);
307
0
    free(key);
308
0
}
309
310
// Deprecated functions kept only for backward API compatibility
311
// LCOV_EXCL_START
312
313
#include <crm/common/xml_compat.h>
314
#include <crm/common/xml_element_compat.h>
315
316
char *
317
calculate_on_disk_digest(xmlNode *input)
318
0
{
319
0
    return calculate_xml_digest_v1(input);
320
0
}
321
322
char *
323
calculate_operation_digest(xmlNode *input, const char *version)
324
0
{
325
0
    xmlNode *sorted = sorted_xml(input, NULL, true);
326
0
    char *digest = calculate_xml_digest_v1(sorted);
327
328
0
    pcmk__xml_free(sorted);
329
0
    return digest;
330
0
}
331
332
char *
333
calculate_xml_versioned_digest(xmlNode *input, gboolean sort,
334
                               gboolean do_filter, const char *version)
335
0
{
336
0
    if ((version == NULL) || (compare_version("3.0.5", version) > 0)) {
337
0
        xmlNode *sorted = NULL;
338
0
        char *digest = NULL;
339
340
0
        if (sort) {
341
0
            xmlNode *sorted = sorted_xml(input, NULL, true);
342
343
0
            input = sorted;
344
0
        }
345
346
0
        crm_trace("Using v1 digest algorithm for %s",
347
0
                  pcmk__s(version, "unknown feature set"));
348
349
0
        digest = calculate_xml_digest_v1(input);
350
351
0
        pcmk__xml_free(sorted);
352
0
        return digest;
353
0
    }
354
0
    crm_trace("Using v2 digest algorithm for %s", version);
355
0
    return pcmk__digest_xml(input, do_filter);
356
0
}
357
358
// LCOV_EXCL_STOP
359
// End deprecated API