diff options
Diffstat (limited to 'lib/common/digest.c')
-rw-r--r-- | lib/common/digest.c | 278 |
1 files changed, 278 insertions, 0 deletions
diff --git a/lib/common/digest.c b/lib/common/digest.c new file mode 100644 index 0000000..3bf04bf --- /dev/null +++ b/lib/common/digest.c @@ -0,0 +1,278 @@ +/* + * Copyright 2015-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#include <stdio.h> +#include <unistd.h> +#include <string.h> +#include <stdlib.h> +#include <md5.h> + +#include <crm/crm.h> +#include <crm/msg_xml.h> +#include <crm/common/xml.h> +#include "crmcommon_private.h" + +#define BEST_EFFORT_STATUS 0 + +/*! + * \internal + * \brief Dump XML in a format used with v1 digests + * + * \param[in] xml Root of XML to dump + * + * \return Newly allocated buffer containing dumped XML + */ +static GString * +dump_xml_for_digest(xmlNodePtr xml) +{ + GString *buffer = g_string_sized_new(1024); + + /* for compatibility with the old result which is used for v1 digests */ + g_string_append_c(buffer, ' '); + pcmk__xml2text(xml, 0, buffer, 0); + g_string_append_c(buffer, '\n'); + + return buffer; +} + +/*! + * \brief Calculate and return v1 digest of XML tree + * + * \param[in] input Root of XML to digest + * \param[in] sort Whether to sort the XML before calculating digest + * \param[in] ignored Not used + * + * \return Newly allocated string containing digest + * \note Example return value: "c048eae664dba840e1d2060f00299e9d" + */ +static char * +calculate_xml_digest_v1(xmlNode *input, gboolean sort, gboolean ignored) +{ + char *digest = NULL; + GString *buffer = NULL; + xmlNode *copy = NULL; + + if (sort) { + crm_trace("Sorting xml..."); + copy = sorted_xml(input, NULL, TRUE); + crm_trace("Done"); + input = copy; + } + + buffer = dump_xml_for_digest(input); + CRM_CHECK(buffer->len > 0, free_xml(copy); + g_string_free(buffer, TRUE); + return NULL); + + digest = crm_md5sum((const char *) buffer->str); + crm_log_xml_trace(input, "digest:source"); + + g_string_free(buffer, TRUE); + free_xml(copy); + return digest; +} + +/*! + * \brief Calculate and return v2 digest of XML tree + * + * \param[in] source Root of XML to digest + * \param[in] do_filter Whether to filter certain XML attributes + * + * \return Newly allocated string containing digest + */ +static char * +calculate_xml_digest_v2(xmlNode *source, gboolean do_filter) +{ + char *digest = NULL; + GString *buffer = g_string_sized_new(1024); + + crm_trace("Begin digest %s", do_filter?"filtered":""); + pcmk__xml2text(source, (do_filter? pcmk__xml_fmt_filtered : 0), buffer, 0); + + CRM_ASSERT(buffer != NULL); + digest = crm_md5sum((const char *) buffer->str); + + pcmk__if_tracing( + { + char *trace_file = crm_strdup_printf("%s/digest-%s", + pcmk__get_tmpdir(), digest); + + crm_trace("Saving %s.%s.%s to %s", + crm_element_value(source, XML_ATTR_GENERATION_ADMIN), + crm_element_value(source, XML_ATTR_GENERATION), + crm_element_value(source, XML_ATTR_NUMUPDATES), + trace_file); + save_xml_to_file(source, "digest input", trace_file); + free(trace_file); + }, + {} + ); + g_string_free(buffer, TRUE); + crm_trace("End digest"); + return digest; +} + +/*! + * \brief Calculate and return digest of XML tree, suitable for storing on disk + * + * \param[in] input Root of XML to digest + * + * \return Newly allocated string containing digest + */ +char * +calculate_on_disk_digest(xmlNode *input) +{ + /* Always use the v1 format for on-disk digests + * a) it's a compatibility nightmare + * b) we only use this once at startup, all other + * invocations are in a separate child process + */ + return calculate_xml_digest_v1(input, FALSE, FALSE); +} + +/*! + * \brief Calculate and return digest of XML operation + * + * \param[in] input Root of XML to digest + * \param[in] version Unused + * + * \return Newly allocated string containing digest + */ +char * +calculate_operation_digest(xmlNode *input, const char *version) +{ + /* We still need the sorting for operation digests */ + return calculate_xml_digest_v1(input, TRUE, FALSE); +} + +/*! + * \brief Calculate and return digest of XML tree + * + * \param[in] input Root of XML to digest + * \param[in] sort Whether to sort XML before calculating digest + * \param[in] do_filter Whether to filter certain XML attributes + * \param[in] version CRM feature set version (used to select v1/v2 digest) + * + * \return Newly allocated string containing digest + */ +char * +calculate_xml_versioned_digest(xmlNode *input, gboolean sort, + gboolean do_filter, const char *version) +{ + /* + * @COMPAT digests (on-disk or in diffs/patchsets) created <1.1.4; + * removing this affects even full-restart upgrades from old versions + * + * The sorting associated with v1 digest creation accounted for 23% of + * the CIB manager's CPU usage on the server. v2 drops this. + * + * The filtering accounts for an additional 2.5% and we may want to + * remove it in future. + * + * v2 also uses the xmlBuffer contents directly to avoid additional copying + */ + if (version == NULL || compare_version("3.0.5", version) > 0) { + crm_trace("Using v1 digest algorithm for %s", + pcmk__s(version, "unknown feature set")); + return calculate_xml_digest_v1(input, sort, do_filter); + } + crm_trace("Using v2 digest algorithm for %s", + pcmk__s(version, "unknown feature set")); + return calculate_xml_digest_v2(input, do_filter); +} + +/*! + * \internal + * \brief Check whether calculated digest of given XML matches expected digest + * + * \param[in] input Root of XML tree to digest + * \param[in] expected Expected digest in on-disk format + * + * \return true if digests match, false on mismatch or error + */ +bool +pcmk__verify_digest(xmlNode *input, const char *expected) +{ + char *calculated = NULL; + bool passed; + + if (input != NULL) { + calculated = calculate_on_disk_digest(input); + if (calculated == NULL) { + crm_perror(LOG_ERR, "Could not calculate digest for comparison"); + return false; + } + } + passed = pcmk__str_eq(expected, calculated, pcmk__str_casei); + if (passed) { + crm_trace("Digest comparison passed: %s", calculated); + } else { + crm_err("Digest comparison failed: expected %s, calculated %s", + expected, calculated); + } + free(calculated); + return passed; +} + +/*! + * \internal + * \brief Check whether an XML attribute should be excluded from CIB digests + * + * \param[in] name XML attribute name + * + * \return true if XML attribute should be excluded from CIB digest calculation + */ +bool +pcmk__xa_filterable(const char *name) +{ + static const char *filter[] = { + XML_ATTR_ORIGIN, + XML_CIB_ATTR_WRITTEN, + XML_ATTR_UPDATE_ORIG, + XML_ATTR_UPDATE_CLIENT, + XML_ATTR_UPDATE_USER, + }; + + for (int i = 0; i < PCMK__NELEM(filter); i++) { + if (strcmp(name, filter[i]) == 0) { + return true; + } + } + return false; +} + +char * +crm_md5sum(const char *buffer) +{ + int lpc = 0, len = 0; + char *digest = NULL; + unsigned char raw_digest[MD5_DIGEST_SIZE]; + + if (buffer == NULL) { + buffer = ""; + } + len = strlen(buffer); + + crm_trace("Beginning digest of %d bytes", len); + digest = malloc(2 * MD5_DIGEST_SIZE + 1); + if (digest) { + md5_buffer(buffer, len, raw_digest); + for (lpc = 0; lpc < MD5_DIGEST_SIZE; lpc++) { + sprintf(digest + (2 * lpc), "%02x", raw_digest[lpc]); + } + digest[(2 * MD5_DIGEST_SIZE)] = 0; + crm_trace("Digest %s.", digest); + + } else { + crm_err("Could not create digest"); + } + return digest; +} |