/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 |