/src/pacemaker/lib/common/digest.c
Line | Count | Source |
1 | | /* |
2 | | * Copyright 2015-2025 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 | | * Pacemaker uses digests (MD5 hashes) of stringified XML to detect changes in |
30 | | * the CIB as a whole, a particular resource's agent parameters, and the device |
31 | | * parameters last used to unfence a particular node. |
32 | | * |
33 | | * "v2" digests hash pcmk__xml_string() directly, while less efficient "v1" |
34 | | * digests do the same with a prefixed space, suffixed newline, and optional |
35 | | * pre-sorting. |
36 | | * |
37 | | * On-disk CIB digests use v1 without sorting. |
38 | | * |
39 | | * Operation digests use v1 with sorting, and are stored in a resource's |
40 | | * operation history in the CIB status section. They come in three flavors: |
41 | | * - a digest of (nearly) all resource parameters and options, used to detect |
42 | | * any resource configuration change; |
43 | | * - a digest of resource parameters marked as nonreloadable, used to decide |
44 | | * whether a reload or full restart is needed after a configuration change; |
45 | | * - and a digest of resource parameters not marked as private, used in |
46 | | * simulations where private parameters have been removed from the input. |
47 | | * |
48 | | * Unfencing digests are set as node attributes, and are used to require |
49 | | * that nodes be unfenced again after a device's configuration changes. |
50 | | */ |
51 | | |
52 | | /*! |
53 | | * \internal |
54 | | * \brief Dump XML in a format used with v1 digests |
55 | | * |
56 | | * \param[in] xml Root of XML to dump |
57 | | * |
58 | | * \return Newly allocated buffer containing dumped XML |
59 | | */ |
60 | | static GString * |
61 | | dump_xml_for_digest(const xmlNode *xml) |
62 | 0 | { |
63 | 0 | GString *buffer = g_string_sized_new(1024); |
64 | | |
65 | | /* for compatibility with the old result which is used for v1 digests */ |
66 | 0 | g_string_append_c(buffer, ' '); |
67 | 0 | pcmk__xml_string(xml, 0, buffer, 0); |
68 | 0 | g_string_append_c(buffer, '\n'); |
69 | |
|
70 | 0 | return buffer; |
71 | 0 | } |
72 | | |
73 | | /*! |
74 | | * \internal |
75 | | * \brief Compute an MD5 checksum for a given input string |
76 | | * |
77 | | * \param[in] input Input string (can be \c NULL) |
78 | | * |
79 | | * \return Newly allocated string containing MD5 checksum for \p input, or |
80 | | * \c NULL on error or if \p input is \c NULL |
81 | | * |
82 | | * \note The caller is responsible for freeing the return value using \c free(). |
83 | | */ |
84 | | char * |
85 | | pcmk__md5sum(const char *input) |
86 | 0 | { |
87 | 0 | char *checksum = NULL; |
88 | 0 | gchar *checksum_g = NULL; |
89 | |
|
90 | 0 | if (input == NULL) { |
91 | 0 | return NULL; |
92 | 0 | } |
93 | | |
94 | | /* g_compute_checksum_for_string() returns NULL if the input string is |
95 | | * empty. There are instances where we may want to hash an empty, but |
96 | | * non-NULL, string, so here we just hardcode the result. |
97 | | */ |
98 | 0 | if (pcmk__str_empty(input)) { |
99 | 0 | return pcmk__str_copy("d41d8cd98f00b204e9800998ecf8427e"); |
100 | 0 | } |
101 | | |
102 | 0 | checksum_g = g_compute_checksum_for_string(G_CHECKSUM_MD5, input, -1); |
103 | 0 | if (checksum_g == NULL) { |
104 | 0 | pcmk__err("Failed to compute MD5 checksum for %s", input); |
105 | 0 | return NULL; |
106 | 0 | } |
107 | | |
108 | | // Make a copy just so that callers can use free() instead of g_free() |
109 | 0 | checksum = pcmk__str_copy(checksum_g); |
110 | 0 | g_free(checksum_g); |
111 | 0 | return checksum; |
112 | 0 | } |
113 | | |
114 | | /*! |
115 | | * \internal |
116 | | * \brief Calculate and return v1 digest of XML tree |
117 | | * |
118 | | * \param[in] input Root of XML to digest |
119 | | * |
120 | | * \return Newly allocated string containing digest |
121 | | * |
122 | | * \note Example return value: "c048eae664dba840e1d2060f00299e9d" |
123 | | */ |
124 | | static char * |
125 | | calculate_xml_digest_v1(const xmlNode *input) |
126 | 0 | { |
127 | 0 | GString *buffer = dump_xml_for_digest(input); |
128 | 0 | char *digest = NULL; |
129 | | |
130 | | // buffer->len > 2 for initial space and trailing newline |
131 | 0 | CRM_CHECK(buffer->len > 2, |
132 | 0 | g_string_free(buffer, TRUE); |
133 | 0 | return NULL); |
134 | | |
135 | 0 | digest = pcmk__md5sum(buffer->str); |
136 | |
|
137 | 0 | g_string_free(buffer, TRUE); |
138 | 0 | return digest; |
139 | 0 | } |
140 | | |
141 | | /*! |
142 | | * \internal |
143 | | * \brief Calculate and return the digest of a CIB, suitable for storing on disk |
144 | | * |
145 | | * \param[in] input Root of XML to digest |
146 | | * |
147 | | * \return Newly allocated string containing digest |
148 | | */ |
149 | | char * |
150 | | pcmk__digest_on_disk_cib(const xmlNode *input) |
151 | 0 | { |
152 | | /* Always use the v1 format for on-disk digests. |
153 | | * * Switching to v2 affects even full-restart upgrades, so it would be a |
154 | | * compatibility nightmare. |
155 | | * * We only use this once at startup. All other invocations are in a |
156 | | * separate child process. |
157 | | */ |
158 | 0 | return calculate_xml_digest_v1(input); |
159 | 0 | } |
160 | | |
161 | | /*! |
162 | | * \internal |
163 | | * \brief Calculate and return digest of a \c PCMK_XE_PARAMETERS element |
164 | | * |
165 | | * This is intended for parameters of a resource operation (also known as |
166 | | * resource action). A \c PCMK_XE_PARAMETERS element from a different source |
167 | | * (for example, resource agent metadata) may have child elements, which are not |
168 | | * allowed here. |
169 | | * |
170 | | * The digest is invariant to changes in the order of XML attributes. |
171 | | * |
172 | | * \param[in] input XML element to digest (must have no children) |
173 | | * |
174 | | * \return Newly allocated string containing digest |
175 | | */ |
176 | | char * |
177 | | pcmk__digest_op_params(const xmlNode *input) |
178 | 0 | { |
179 | | /* Switching to v2 digests would likely cause restarts during rolling |
180 | | * upgrades. |
181 | | * |
182 | | * @TODO Confirm this. Switch to v2 if safe, or drop this TODO otherwise. |
183 | | */ |
184 | 0 | char *digest = NULL; |
185 | 0 | xmlNode *sorted = NULL; |
186 | |
|
187 | 0 | pcmk__assert(input->children == NULL); |
188 | |
|
189 | 0 | sorted = pcmk__xe_create(NULL, (const char *) input->name); |
190 | 0 | pcmk__xe_copy_attrs(sorted, input, pcmk__xaf_none); |
191 | 0 | pcmk__xe_sort_attrs(sorted); |
192 | |
|
193 | 0 | digest = calculate_xml_digest_v1(sorted); |
194 | |
|
195 | 0 | pcmk__xml_free(sorted); |
196 | 0 | return digest; |
197 | 0 | } |
198 | | |
199 | | /*! |
200 | | * \internal |
201 | | * \brief Calculate and return the digest of an XML tree |
202 | | * |
203 | | * \param[in] xml XML tree to digest |
204 | | * \param[in] filter Whether to filter certain XML attributes |
205 | | * |
206 | | * \return Newly allocated string containing digest |
207 | | */ |
208 | | char * |
209 | | pcmk__digest_xml(const xmlNode *xml, bool filter) |
210 | 0 | { |
211 | | /* @TODO Filtering accounts for significant CPU usage. Consider removing if |
212 | | * possible. |
213 | | */ |
214 | 0 | char *digest = NULL; |
215 | 0 | GString *buf = g_string_sized_new(1024); |
216 | |
|
217 | 0 | pcmk__xml_string(xml, (filter? pcmk__xml_fmt_filtered : 0), buf, 0); |
218 | 0 | digest = pcmk__md5sum(buf->str); |
219 | 0 | if (digest == NULL) { |
220 | 0 | goto done; |
221 | 0 | } |
222 | | |
223 | 0 | pcmk__if_tracing( |
224 | 0 | { |
225 | 0 | char *trace_file = pcmk__assert_asprintf("digest-%s", digest); |
226 | |
|
227 | 0 | pcmk__trace("Saving %s.%s.%s to %s", |
228 | 0 | pcmk__xe_get(xml, PCMK_XA_ADMIN_EPOCH), |
229 | 0 | pcmk__xe_get(xml, PCMK_XA_EPOCH), |
230 | 0 | pcmk__xe_get(xml, PCMK_XA_NUM_UPDATES), trace_file); |
231 | 0 | pcmk__xml_write_temp_file(xml, "digest input", trace_file); |
232 | 0 | free(trace_file); |
233 | 0 | }, |
234 | 0 | {} |
235 | 0 | ); |
236 | | |
237 | 0 | done: |
238 | 0 | g_string_free(buf, TRUE); |
239 | 0 | return digest; |
240 | 0 | } |
241 | | |
242 | | /*! |
243 | | * \internal |
244 | | * \brief Check whether calculated digest of given XML matches expected digest |
245 | | * |
246 | | * \param[in] input Root of XML tree to digest |
247 | | * \param[in] expected Expected digest in on-disk format |
248 | | * |
249 | | * \return true if digests match, false on mismatch or error |
250 | | */ |
251 | | bool |
252 | | pcmk__verify_digest(const xmlNode *input, const char *expected) |
253 | 0 | { |
254 | 0 | char *calculated = NULL; |
255 | 0 | bool passed; |
256 | |
|
257 | 0 | if (input != NULL) { |
258 | 0 | calculated = pcmk__digest_on_disk_cib(input); |
259 | 0 | if (calculated == NULL) { |
260 | 0 | pcmk__err("Could not calculate digest for comparison"); |
261 | 0 | return false; |
262 | 0 | } |
263 | 0 | } |
264 | 0 | passed = pcmk__str_eq(expected, calculated, pcmk__str_casei); |
265 | 0 | if (passed) { |
266 | 0 | pcmk__trace("Digest comparison passed: %s", calculated); |
267 | 0 | } else { |
268 | 0 | pcmk__err("Digest comparison failed: expected %s, calculated %s", |
269 | 0 | expected, calculated); |
270 | 0 | } |
271 | 0 | free(calculated); |
272 | 0 | return passed; |
273 | 0 | } |
274 | | |
275 | | /*! |
276 | | * \internal |
277 | | * \brief Check whether an XML attribute should be excluded from CIB digests |
278 | | * |
279 | | * \param[in] name XML attribute name |
280 | | * |
281 | | * \return true if XML attribute should be excluded from CIB digest calculation |
282 | | */ |
283 | | bool |
284 | | pcmk__xa_filterable(const char *name) |
285 | 0 | { |
286 | 0 | static const char *filter[] = { |
287 | 0 | PCMK_XA_CRM_DEBUG_ORIGIN, |
288 | 0 | PCMK_XA_CIB_LAST_WRITTEN, |
289 | 0 | PCMK_XA_UPDATE_ORIGIN, |
290 | 0 | PCMK_XA_UPDATE_CLIENT, |
291 | 0 | PCMK_XA_UPDATE_USER, |
292 | 0 | }; |
293 | |
|
294 | 0 | for (int i = 0; i < PCMK__NELEM(filter); i++) { |
295 | 0 | if (strcmp(name, filter[i]) == 0) { |
296 | 0 | return true; |
297 | 0 | } |
298 | 0 | } |
299 | 0 | return false; |
300 | 0 | } |
301 | | |
302 | | // Return true if a is an attribute that should be filtered |
303 | | static bool |
304 | | should_filter_for_digest(xmlAttrPtr a, void *user_data) |
305 | 0 | { |
306 | 0 | if (strncmp((const char *) a->name, CRM_META "_", |
307 | 0 | sizeof(CRM_META " ") - 1) == 0) { |
308 | 0 | return true; |
309 | 0 | } |
310 | 0 | return pcmk__str_any_of((const char *) a->name, |
311 | 0 | PCMK_XA_ID, |
312 | 0 | PCMK_XA_CRM_FEATURE_SET, |
313 | 0 | PCMK__XA_OP_DIGEST, |
314 | 0 | PCMK__META_ON_NODE, |
315 | 0 | PCMK__META_ON_NODE_UUID, |
316 | 0 | "pcmk_external_ip", |
317 | 0 | NULL); |
318 | 0 | } |
319 | | |
320 | | /*! |
321 | | * \internal |
322 | | * \brief Remove XML attributes not needed for operation digest |
323 | | * |
324 | | * \param[in,out] param_set XML with operation parameters |
325 | | */ |
326 | | void |
327 | | pcmk__filter_op_for_digest(xmlNode *param_set) |
328 | 0 | { |
329 | 0 | char *key = NULL; |
330 | 0 | char *timeout = NULL; |
331 | 0 | guint interval_ms = 0; |
332 | |
|
333 | 0 | if (param_set == NULL) { |
334 | 0 | return; |
335 | 0 | } |
336 | | |
337 | | /* Timeout is useful for recurring operation digests, so grab it before |
338 | | * removing meta-attributes |
339 | | */ |
340 | 0 | key = crm_meta_name(PCMK_META_INTERVAL); |
341 | 0 | pcmk__xe_get_guint(param_set, key, &interval_ms); |
342 | 0 | free(key); |
343 | 0 | key = NULL; |
344 | 0 | if (interval_ms != 0) { |
345 | 0 | key = crm_meta_name(PCMK_META_TIMEOUT); |
346 | 0 | timeout = pcmk__xe_get_copy(param_set, key); |
347 | 0 | } |
348 | | |
349 | | // Remove all CRM_meta_* attributes and certain other attributes |
350 | 0 | pcmk__xe_remove_matching_attrs(param_set, false, should_filter_for_digest, |
351 | 0 | NULL); |
352 | | |
353 | | // Add timeout back for recurring operation digests |
354 | 0 | if (timeout != NULL) { |
355 | 0 | pcmk__xe_set(param_set, key, timeout); |
356 | 0 | } |
357 | 0 | free(timeout); |
358 | 0 | free(key); |
359 | 0 | } |
360 | | |
361 | | // Deprecated functions kept only for backward API compatibility |
362 | | // LCOV_EXCL_START |
363 | | |
364 | | #include <crm/common/util_compat.h> // crm_md5sum() |
365 | | #include <crm/common/xml_compat.h> |
366 | | #include <crm/common/xml_element_compat.h> |
367 | | |
368 | | char * |
369 | | calculate_on_disk_digest(xmlNode *input) |
370 | 0 | { |
371 | 0 | return calculate_xml_digest_v1(input); |
372 | 0 | } |
373 | | |
374 | | char * |
375 | | calculate_operation_digest(xmlNode *input, const char *version) |
376 | 0 | { |
377 | 0 | xmlNode *sorted = sorted_xml(input, NULL, true); |
378 | 0 | char *digest = calculate_xml_digest_v1(sorted); |
379 | |
|
380 | 0 | pcmk__xml_free(sorted); |
381 | 0 | return digest; |
382 | 0 | } |
383 | | |
384 | | char * |
385 | | calculate_xml_versioned_digest(xmlNode *input, gboolean sort, |
386 | | gboolean do_filter, const char *version) |
387 | 0 | { |
388 | 0 | if ((version == NULL) || (pcmk__compare_versions("3.0.5", version) > 0)) { |
389 | 0 | xmlNode *sorted = NULL; |
390 | 0 | char *digest = NULL; |
391 | |
|
392 | 0 | if (sort) { |
393 | 0 | xmlNode *sorted = sorted_xml(input, NULL, true); |
394 | |
|
395 | 0 | input = sorted; |
396 | 0 | } |
397 | |
|
398 | 0 | pcmk__trace("Using v1 digest algorithm for %s", |
399 | 0 | pcmk__s(version, "unknown feature set")); |
400 | | |
401 | 0 | digest = calculate_xml_digest_v1(input); |
402 | |
|
403 | 0 | pcmk__xml_free(sorted); |
404 | 0 | return digest; |
405 | 0 | } |
406 | 0 | pcmk__trace("Using v2 digest algorithm for %s", version); |
407 | 0 | return pcmk__digest_xml(input, do_filter); |
408 | 0 | } |
409 | | |
410 | | char * |
411 | | crm_md5sum(const char *buffer) |
412 | 0 | { |
413 | 0 | char *digest = NULL; |
414 | 0 | gchar *raw_digest = NULL; |
415 | | |
416 | | /* g_compute_checksum_for_string returns NULL if the input string is empty. |
417 | | * There are instances where we may want to hash an empty, but non-NULL, |
418 | | * string so here we just hardcode the result. |
419 | | */ |
420 | 0 | if (buffer == NULL) { |
421 | 0 | return NULL; |
422 | 0 | } else if (pcmk__str_empty(buffer)) { |
423 | 0 | return pcmk__str_copy("d41d8cd98f00b204e9800998ecf8427e"); |
424 | 0 | } |
425 | | |
426 | 0 | raw_digest = g_compute_checksum_for_string(G_CHECKSUM_MD5, buffer, -1); |
427 | |
|
428 | 0 | if (raw_digest == NULL) { |
429 | 0 | pcmk__err("Failed to calculate hash"); |
430 | 0 | return NULL; |
431 | 0 | } |
432 | | |
433 | 0 | digest = pcmk__str_copy(raw_digest); |
434 | 0 | g_free(raw_digest); |
435 | |
|
436 | 0 | pcmk__trace("Digest %s.", digest); |
437 | 0 | return digest; |
438 | 0 | } |
439 | | |
440 | | // LCOV_EXCL_STOP |
441 | | // End deprecated API |