From 62f9695ed8fda0074860d915fa9ffe67cac15cf7 Mon Sep 17 00:00:00 2001 From: Oliver Kurth Date: Fri, 16 Jun 2023 16:29:07 -0700 Subject: [PATCH 1/5] Merge pull request #424 from oliverkurth/topic/okurth/checksize check rpm file size with the size that is expected from repo data --- client/defines.h | 5 +++-- client/prototypes.h | 3 +-- client/rpmtrans.c | 27 +++++++++++++++++++++++---- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/client/defines.h b/client/defines.h index 602b2498..ede24a33 100644 --- a/client/defines.h +++ b/client/defines.h @@ -236,8 +236,8 @@ typedef enum {ERROR_TDNF_METADATA_EXPIRE_PARSE, "ERROR_TDNF_METADATA_EXPIRE_PARSE", "metadata_expire value could not be parsed. Check your repo files."},\ {ERROR_TDNF_PROTECTED, "ERROR_TDNF_PROTECTED", "The operation would result in removing a protected package."},\ {ERROR_TDNF_DOWNGRADE_NOT_ALLOWED,\ - "ERROR_TDNF_DOWNGRADE_NOT_ALLOWED",\ - "a downgrade is not allowed below the minimal version. Check 'minversions' in the configuration."},\ + "ERROR_TDNF_DOWNGRADE_NOT_ALLOWED",\ + "a downgrade is not allowed below the minimal version. Check 'minversions' in the configuration."},\ {ERROR_TDNF_PERM, "ERROR_TDNF_PERM", "Operation not permitted. You have to be root."},\ {ERROR_TDNF_OPT_NOT_FOUND, "ERROR_TDNF_OPT_NOT_FOUND", "A required option was not found"},\ {ERROR_TDNF_OPERATION_ABORTED, "ERROR_TDNF_OPERATION_ABORTED", "Operation aborted."},\ @@ -248,6 +248,7 @@ typedef enum {ERROR_TDNF_EVENT_CTXT_ITEM_INVALID_TYPE, "ERROR_TDNF_EVENT_CTXT_ITEM_INVALID_TYPE", "An event item type had a mismatch. This is usually related to plugin events. Try --noplugins to deactivate all plugins or --disableplugin= to deactivate a specific one. You can permanently deactivate an offending plugin by setting enable=0 in the plugin config file."},\ {ERROR_TDNF_NO_GPGKEY_CONF_ENTRY, "ERROR_TDNF_NO_GPGKEY_CONF_ENTRY", "gpgkey entry is missing for this repo. please add gpgkey in repo file or use --nogpgcheck to ignore."}, \ {ERROR_TDNF_URL_INVALID, "ERROR_TDNF_URL_INVALID", "URL is invalid."}, \ + {ERROR_TDNF_SIZE_MISMATCH, "ERROR_TDNF_SIZE_MISMATCH", "File size does not match."}, \ {ERROR_TDNF_BASEURL_DOES_NOT_EXISTS, "ERROR_TDNF_BASEURL_DOES_NOT_EXISTS", "Base URL and Metalink URL not found in the repo file"},\ {ERROR_TDNF_CHECKSUM_VALIDATION_FAILED, "ERROR_TDNF_CHECKSUM_VALIDATION_FAILED", "Checksum Validation failed for the repomd.xml downloaded using URL from metalink"},\ {ERROR_TDNF_METALINK_RESOURCE_VALIDATION_FAILED, "ERROR_TDNF_METALINK_RESOURCE_VALIDATION_FAILED", "No Resource present in metalink file for file download"},\ diff --git a/client/prototypes.h b/client/prototypes.h index 9ff3e7d0..ba9677d8 100644 --- a/client/prototypes.h +++ b/client/prototypes.h @@ -871,8 +871,7 @@ uint32_t TDNFTransAddInstallPkg( PTDNFRPMTS pTS, PTDNF pTdnf, - const char* pszPackageLocation, - const char* pszPkgName, + PTDNF_PKG_INFO pInfo, PTDNF_REPO_DATA pRepo, int nUpgrade ); diff --git a/client/rpmtrans.c b/client/rpmtrans.c index 2134b4a2..f5d122ed 100644 --- a/client/rpmtrans.c +++ b/client/rpmtrans.c @@ -799,8 +799,7 @@ TDNFTransAddInstallPkgs( dwError = TDNFTransAddInstallPkg( pTS, pTdnf, - pInfo->pszLocation, - pInfo->pszName, + pInfo, pRepo, nUpgrade); BAIL_ON_TDNF_ERROR(dwError); @@ -821,8 +820,7 @@ uint32_t TDNFTransAddInstallPkg( PTDNFRPMTS pTS, PTDNF pTdnf, - const char* pszPackageLocation, - const char* pszPkgName, + PTDNF_PKG_INFO pInfo, PTDNF_REPO_DATA pRepo, int nUpgrade ) @@ -832,6 +830,18 @@ TDNFTransAddInstallPkg( char* pszFilePath = NULL; Header rpmHeader = NULL; PTDNF_CACHED_RPM_ENTRY pRpmCache = NULL; + const char* pszPackageLocation = NULL; + const char* pszPkgName = NULL; + int nSize; + + if(!pTS || !pTdnf || !pInfo || !pRepo) + { + dwError = ERROR_TDNF_INVALID_PARAMETER; + BAIL_ON_TDNF_ERROR(dwError); + } + + pszPackageLocation = pInfo->pszLocation; + pszPkgName = pInfo->pszName; if (pszPackageLocation[0] == '/') { @@ -900,6 +910,15 @@ TDNFTransAddInstallPkg( BAIL_ON_TDNF_SYSTEM_ERROR(dwError); } + dwError = TDNFGetFileSize(pszFilePath, &nSize); + BAIL_ON_TDNF_ERROR(dwError); + + if (nSize != (int)pInfo->dwDownloadSizeBytes) { + pr_err("rpm file (%s) size (%u) does not match expected size (%u)\n", pszFilePath, nSize, pInfo->dwDownloadSizeBytes); + dwError = ERROR_TDNF_SIZE_MISMATCH; + BAIL_ON_TDNF_ERROR(dwError); + } + dwError = TDNFGPGCheckPackage(pTS, pTdnf, pRepo, pszFilePath, &rpmHeader); BAIL_ON_TDNF_ERROR(dwError); From ac69a25fff31924f63858e147676a49bd5cacabe Mon Sep 17 00:00:00 2001 From: Shivani Agarwal Date: Tue, 15 Aug 2023 09:56:45 +0000 Subject: [PATCH 2/5] Compare checksum for repodata against the downloaded rpm file --- client/defines.h | 1 + client/includes.h | 9 - client/packageutils.c | 29 ++++ client/prototypes.h | 3 - client/rpmtrans.c | 16 ++ common/prototypes.h | 33 ++++ common/structs.h | 27 +++ common/utils.c | 303 ++++++++++++++++++++++++++++++++- include/tdnferror.h | 3 +- include/tdnftypes.h | 2 + plugins/metalink/utils.c | 286 ------------------------------- pytests/repo/setup-repo.sh | 14 ++ pytests/tests/test_checksum.py | 88 ++++++++++ pytests/tests/test_metalink.py | 6 +- solv/prototypes.h | 7 + solv/tdnfpackage.c | 56 ++++++ 16 files changed, 580 insertions(+), 303 deletions(-) create mode 100644 pytests/tests/test_checksum.py diff --git a/client/defines.h b/client/defines.h index ede24a33..9e28591a 100644 --- a/client/defines.h +++ b/client/defines.h @@ -249,6 +249,7 @@ typedef enum {ERROR_TDNF_NO_GPGKEY_CONF_ENTRY, "ERROR_TDNF_NO_GPGKEY_CONF_ENTRY", "gpgkey entry is missing for this repo. please add gpgkey in repo file or use --nogpgcheck to ignore."}, \ {ERROR_TDNF_URL_INVALID, "ERROR_TDNF_URL_INVALID", "URL is invalid."}, \ {ERROR_TDNF_SIZE_MISMATCH, "ERROR_TDNF_SIZE_MISMATCH", "File size does not match."}, \ + {ERROR_TDNF_CHECKSUM_MISMATCH, "ERROR_TDNF_CHECKSUM_MISMATCH", "File checksum does not match."}, \ {ERROR_TDNF_BASEURL_DOES_NOT_EXISTS, "ERROR_TDNF_BASEURL_DOES_NOT_EXISTS", "Base URL and Metalink URL not found in the repo file"},\ {ERROR_TDNF_CHECKSUM_VALIDATION_FAILED, "ERROR_TDNF_CHECKSUM_VALIDATION_FAILED", "Checksum Validation failed for the repomd.xml downloaded using URL from metalink"},\ {ERROR_TDNF_METALINK_RESOURCE_VALIDATION_FAILED, "ERROR_TDNF_METALINK_RESOURCE_VALIDATION_FAILED", "No Resource present in metalink file for file download"},\ diff --git a/client/includes.h b/client/includes.h index 93fee6e9..38c75aa2 100644 --- a/client/includes.h +++ b/client/includes.h @@ -63,13 +63,4 @@ #include "config.h" -// Enum in order of preference -enum { - TDNF_HASH_MD5 = 0, - TDNF_HASH_SHA1, - TDNF_HASH_SHA256, - TDNF_HASH_SHA512, - TDNF_HASH_SENTINEL -}; - #endif /* __CLIENT_INCLUDES_H__ */ diff --git a/client/packageutils.c b/client/packageutils.c index adc38707..84858e96 100644 --- a/client/packageutils.c +++ b/client/packageutils.c @@ -944,6 +944,7 @@ TDNFPopulatePkgInfos( Id dwPkgId = 0; PTDNF_PKG_INFO pPkgInfos = NULL; PTDNF_PKG_INFO pPkgInfo = NULL; + int nChecksumType = 0; if(!ppPkgInfos) { @@ -998,6 +999,34 @@ TDNFPopulatePkgInfos( &pPkgInfo->pszLocation); BAIL_ON_TDNF_ERROR(dwError); + dwError = SolvGetPkgChecksumFromId( + pSack, + dwPkgId, + &nChecksumType, + &pPkgInfo->pbChecksum); + //Ignore no data + if(dwError == ERROR_TDNF_NO_DATA) + { + dwError = 0; + } else if (nChecksumType == REPOKEY_TYPE_SHA512) + { + pPkgInfo->nChecksumType = TDNF_HASH_SHA512; + } else if (nChecksumType == REPOKEY_TYPE_SHA256) + { + pPkgInfo->nChecksumType = TDNF_HASH_SHA256; + } else if (nChecksumType == REPOKEY_TYPE_SHA1) + { + pPkgInfo->nChecksumType = TDNF_HASH_SHA1; + } else if (nChecksumType == REPOKEY_TYPE_MD5) + { + pPkgInfo->nChecksumType = TDNF_HASH_MD5; + } else + { + pPkgInfo->pbChecksum = NULL; + } + + BAIL_ON_TDNF_ERROR(dwError); + dwError = SolvGetPkgInstallSizeFromId( pSack, dwPkgId, diff --git a/client/prototypes.h b/client/prototypes.h index ba9677d8..bb7fba1f 100644 --- a/client/prototypes.h +++ b/client/prototypes.h @@ -22,9 +22,6 @@ #define __CLIENT_PROTOTYPES_H__ #include -#include -#include -#include extern uid_t gEuid; diff --git a/client/rpmtrans.c b/client/rpmtrans.c index f5d122ed..e0940b26 100644 --- a/client/rpmtrans.c +++ b/client/rpmtrans.c @@ -832,6 +832,8 @@ TDNFTransAddInstallPkg( PTDNF_CACHED_RPM_ENTRY pRpmCache = NULL; const char* pszPackageLocation = NULL; const char* pszPkgName = NULL; + uint8_t digest_from_file[EVP_MAX_MD_SIZE] = {0}; + hash_op *hash = NULL; int nSize; if(!pTS || !pTdnf || !pInfo || !pRepo) @@ -910,6 +912,20 @@ TDNFTransAddInstallPkg( BAIL_ON_TDNF_SYSTEM_ERROR(dwError); } + if(pInfo->pbChecksum != NULL) { + hash = hash_ops + pInfo->nChecksumType; + + dwError = TDNFGetDigestForFile(pszFilePath, hash, digest_from_file); + BAIL_ON_TDNF_ERROR(dwError); + + if (memcmp(digest_from_file, pInfo->pbChecksum, hash->length)) + { + pr_err("rpm file (%s) Checksum FAILED (digest mismatch)\n", pszFilePath); + dwError = ERROR_TDNF_CHECKSUM_MISMATCH; + BAIL_ON_TDNF_ERROR(dwError); + } + } + dwError = TDNFGetFileSize(pszFilePath, &nSize); BAIL_ON_TDNF_ERROR(dwError); diff --git a/common/prototypes.h b/common/prototypes.h index 6747240b..1ec6b036 100644 --- a/common/prototypes.h +++ b/common/prototypes.h @@ -324,4 +324,37 @@ tdnflockNewAcquire( const char *descr ); +int32_t strtoi(const char *ptr); + +uint32_t +TDNFGetDigestForFile( + const char *filename, + hash_op *hash, + uint8_t *digest + ); + +uint32_t +TDNFCheckHash( + const char *filename, + unsigned char *digest, + int type + ); + +uint32_t +TDNFCheckHexDigest( + const char *hex_digest, + int digest_length + ); + +uint32_t +TDNFHexToUint( + const char *hex_digest, + unsigned char *uintValue + ); + +uint32_t +TDNFChecksumFromHexDigest( + const char *hex_digest, + unsigned char *ppdigest + ); #endif /* __COMMON_PROTOTYPES_H__ */ diff --git a/common/structs.h b/common/structs.h index 602a1d23..b510b957 100644 --- a/common/structs.h +++ b/common/structs.h @@ -1,5 +1,9 @@ #pragma once +#include +#include +#include + typedef struct _KEYVALUE_ { char *pszKey; @@ -46,3 +50,26 @@ enum { TDNFLOCK_WRITE = 1 << 1, TDNFLOCK_WAIT = 1 << 2, }; + +// Enum in order of preference +enum { + TDNF_HASH_MD5 = 0, + TDNF_HASH_SHA1, + TDNF_HASH_SHA256, + TDNF_HASH_SHA512, + TDNF_HASH_SENTINEL +}; + +typedef struct _hash_op { + char *hash_type; + unsigned int length; +} hash_op; + +typedef struct _hash_type { + char *hash_name; + unsigned int hash_value; +}hash_type; + +extern hash_op hash_ops[TDNF_HASH_SENTINEL]; + +extern hash_type hashType[7]; diff --git a/common/utils.c b/common/utils.c index 2fef6963..d7bb3c9c 100644 --- a/common/utils.c +++ b/common/utils.c @@ -9,6 +9,25 @@ #include #include "includes.h" +hash_op hash_ops[TDNF_HASH_SENTINEL] = + { + [TDNF_HASH_MD5] = {"md5", MD5_DIGEST_LENGTH}, + [TDNF_HASH_SHA1] = {"sha1", SHA_DIGEST_LENGTH}, + [TDNF_HASH_SHA256] = {"sha256", SHA256_DIGEST_LENGTH}, + [TDNF_HASH_SHA512] = {"sha512", SHA512_DIGEST_LENGTH}, + }; + +hash_type hashType[] = + { + {"md5", TDNF_HASH_MD5}, + {"sha1", TDNF_HASH_SHA1}, + {"sha-1", TDNF_HASH_SHA1}, + {"sha256", TDNF_HASH_SHA256}, + {"sha-256", TDNF_HASH_SHA256}, + {"sha512", TDNF_HASH_SHA512}, + {"sha-512", TDNF_HASH_SHA512} + }; + uint32_t TDNFFileReadAllText( const char *pszFileName, @@ -250,6 +269,7 @@ TDNFFreePackageInfoContents( TDNF_SAFE_FREE_MEMORY(pPkgInfo->pszRelease); TDNF_SAFE_FREE_MEMORY(pPkgInfo->pszLocation); TDNF_SAFE_FREE_STRINGARRAY(pPkgInfo->ppszDependencies); + TDNF_SAFE_FREE_MEMORY(pPkgInfo->pbChecksum); TDNF_SAFE_FREE_STRINGARRAY(pPkgInfo->ppszFileList); for (pEntry = pPkgInfo->pChangeLogEntries; pEntry; @@ -954,4 +974,285 @@ TDNFDirName( error: TDNF_SAFE_FREE_MEMORY(pszDirName); goto cleanup; -} \ No newline at end of file +} + +int32_t strtoi(const char *ptr) +{ + char *p = NULL; + long int tmp = 0; + + tmp = strtol(ptr, &p, 10); + + if (*p || tmp > INT_MAX || tmp < INT_MIN) + { + pr_crit("WARNING: invalid arg to %s: '%s'\n", __func__, ptr); + return 0; + } + + return (int32_t) tmp; +} + +int isTrue(const char *str) +{ + if (!strcasecmp(str, "false")) + return 0; + + return !strcasecmp(str, "true") || strtoi(str); +} + +uint32_t +TDNFGetDigestForFile( + const char *filename, + hash_op *hash, + uint8_t *digest + ) +{ + uint32_t dwError = 0; + int fd = -1; + char buf[BUFSIZ] = {0}; + int length = 0; + EVP_MD_CTX *ctx = NULL; + const EVP_MD *digest_type = NULL; + unsigned int digest_length = 0; + + if (IsNullOrEmptyString(filename) || !hash || !digest) + { + dwError = ERROR_TDNF_INVALID_PARAMETER; + BAIL_ON_TDNF_ERROR(dwError); + } + + fd = open(filename, O_RDONLY); + if (fd < 0) + { + pr_err("ERROR: Checksum validating (%s) FAILED\n", filename); + dwError = errno; + BAIL_ON_TDNF_SYSTEM_ERROR_UNCOND(dwError); + } + + digest_type = EVP_get_digestbyname(hash->hash_type); + + if (!digest_type) + { + pr_err("Unknown message digest %s\n", hash->hash_type); + dwError = ERROR_TDNF_INVALID_PARAMETER; + BAIL_ON_TDNF_ERROR(dwError); + } + + ctx = EVP_MD_CTX_create(); + if (!ctx) + { + pr_err("Context Create Failed\n"); + dwError = ERROR_TDNF_CHECKSUM_VALIDATION_FAILED; + BAIL_ON_TDNF_ERROR(dwError); + } + + dwError = EVP_DigestInit_ex(ctx, digest_type, NULL); + if (!dwError) + { + pr_err("Digest Init Failed\n"); + dwError = ERROR_TDNF_CHECKSUM_VALIDATION_FAILED; + /* MD5 is not approved in FIPS mode. So, overrriding + the dwError to show the right error to the user */ +#if defined(OPENSSL_VERSION_MAJOR) && (OPENSSL_VERSION_MAJOR >= 3) + if (EVP_default_properties_is_fips_enabled(NULL) && !strcasecmp(hash->hash_type, "md5")) +#else + if (FIPS_mode() && !strcasecmp(hash->hash_type, "md5")) +#endif + { + dwError = ERROR_TDNF_FIPS_MODE_FORBIDDEN; + } + BAIL_ON_TDNF_ERROR(dwError); + } + + while ((length = read(fd, buf, BUFSIZ - 1)) > 0) + { + dwError = EVP_DigestUpdate(ctx, buf, length); + if (!dwError) + { + pr_err("Digest Update Failed\n"); + dwError = ERROR_TDNF_CHECKSUM_VALIDATION_FAILED; + BAIL_ON_TDNF_ERROR(dwError); + } + memset(buf, 0, BUFSIZ); + } + + if (length == -1) + { + pr_err("Error: Checksum validating (%s) FAILED\n", filename); + dwError = errno; + BAIL_ON_TDNF_SYSTEM_ERROR(dwError); + } + + dwError = EVP_DigestFinal_ex(ctx, digest, &digest_length); + if (!dwError) + { + pr_err("Digest Final Failed\n"); + dwError = ERROR_TDNF_CHECKSUM_VALIDATION_FAILED; + BAIL_ON_TDNF_ERROR(dwError); + } + dwError = 0; + +cleanup: + if (fd >= 0) + { + close(fd); + } + if (ctx) + { + EVP_MD_CTX_destroy(ctx); + } + return dwError; +error: + goto cleanup; +} + +uint32_t +TDNFCheckHash( + const char *filename, + unsigned char *digest, + int type + ) +{ + + uint32_t dwError = 0; + uint8_t digest_from_file[EVP_MAX_MD_SIZE] = {0}; + hash_op *hash = NULL; + + if (IsNullOrEmptyString(filename) || + !digest) + { + dwError = ERROR_TDNF_INVALID_PARAMETER; + BAIL_ON_TDNF_ERROR(dwError); + } + + if (type < TDNF_HASH_MD5 || type >= TDNF_HASH_SENTINEL) + { + dwError = ERROR_TDNF_INVALID_PARAMETER; + BAIL_ON_TDNF_ERROR(dwError); + } + + hash = hash_ops + type; + + dwError = TDNFGetDigestForFile(filename, hash, digest_from_file); + BAIL_ON_TDNF_ERROR(dwError); + + if (memcmp(digest_from_file, digest, hash->length)) + { + dwError = ERROR_TDNF_CHECKSUM_VALIDATION_FAILED; + BAIL_ON_TDNF_ERROR(dwError); + } + +cleanup: + return dwError; +error: + if (!IsNullOrEmptyString(filename)) + { + pr_err("Error: Validating Checksum (%s) FAILED (digest mismatch)\n", filename); + } + goto cleanup; +} + +/* Returns nonzero if hex_digest is properly formatted; that is each + letter is in [0-9A-Za-z] and the length of the string equals to the + result length of digest * 2. */ +uint32_t +TDNFCheckHexDigest( + const char *hex_digest, + int digest_length + ) +{ + int i = 0; + + if(IsNullOrEmptyString(hex_digest) || + (digest_length <= 0)) + { + return 0; + } + + for(i = 0; hex_digest[i]; ++i) + { + if(!isxdigit(hex_digest[i])) + { + return 0; + } + } + + return digest_length * 2 == i; +} + +uint32_t +TDNFHexToUint( + const char *hex_digest, + unsigned char *uintValue + ) +{ + uint32_t dwError = 0; + char buf[3] = {0}; + unsigned long val = 0; + + if(IsNullOrEmptyString(hex_digest) || + !uintValue) + { + dwError = ERROR_TDNF_INVALID_PARAMETER; + BAIL_ON_TDNF_ERROR(dwError); + } + + buf[0] = hex_digest[0]; + buf[1] = hex_digest[1]; + + errno = 0; + val = strtoul(buf, NULL, 16); + if(errno) + { + pr_err("Error: strtoul call failed\n"); + dwError = errno; + BAIL_ON_TDNF_SYSTEM_ERROR(dwError); + } + *uintValue = (unsigned char)(val&0xff); + +cleanup: + return dwError; +error: + goto cleanup; +} + +uint32_t +TDNFChecksumFromHexDigest( + const char *hex_digest, + unsigned char *ppdigest + ) +{ + uint32_t dwError = 0; + unsigned char *pdigest = NULL; + size_t i = 0; + size_t len = 0; + unsigned char uintValue = 0; + + if(IsNullOrEmptyString(hex_digest) || + !ppdigest) + { + dwError = ERROR_TDNF_INVALID_PARAMETER; + BAIL_ON_TDNF_ERROR(dwError); + } + + len = strlen(hex_digest); + + dwError = TDNFAllocateMemory(1, len/2, (void **)&pdigest); + BAIL_ON_TDNF_ERROR(dwError); + + for(i = 0; i < len; i += 2) + { + dwError = TDNFHexToUint(hex_digest + i, &uintValue); + BAIL_ON_TDNF_ERROR(dwError); + + pdigest[i>>1] = uintValue; + } + memcpy( ppdigest, pdigest, len>>1 ); + +cleanup: + TDNF_SAFE_FREE_MEMORY(pdigest); + return dwError; + +error: + goto cleanup; +} diff --git a/include/tdnferror.h b/include/tdnferror.h index 12653d1c..7e6246f7 100644 --- a/include/tdnferror.h +++ b/include/tdnferror.h @@ -152,8 +152,9 @@ extern "C" { #define ERROR_TDNF_RPMTS_OPENDB_FAILED 1526 #define ERROR_TDNF_SIZE_MISMATCH 1527 +#define ERROR_TDNF_CHECKSUM_MISMATCH 1528 -#define ERROR_TDNF_RPMTS_FDDUP_FAILED 1528 +#define ERROR_TDNF_RPMTS_FDDUP_FAILED 1529 /* event context */ #define ERROR_TDNF_EVENT_CTXT_ITEM_NOT_FOUND 1551 diff --git a/include/tdnftypes.h b/include/tdnftypes.h index 2d9e8005..a806010f 100644 --- a/include/tdnftypes.h +++ b/include/tdnftypes.h @@ -153,6 +153,7 @@ typedef struct _TDNF_PKG_INFO uint32_t dwEpoch; uint32_t dwInstallSizeBytes; uint32_t dwDownloadSizeBytes; + int nChecksumType; char* pszName; char* pszRepoName; char* pszVersion; @@ -169,6 +170,7 @@ typedef struct _TDNF_PKG_INFO char **ppszDependencies; char **ppszFileList; char *pszSourcePkg; + unsigned char* pbChecksum; PTDNF_PKG_CHANGELOG_ENTRY pChangeLogEntries; struct _TDNF_PKG_INFO* pNext; }TDNF_PKG_INFO, *PTDNF_PKG_INFO; diff --git a/plugins/metalink/utils.c b/plugins/metalink/utils.c index 8e9761e9..5b58078b 100644 --- a/plugins/metalink/utils.c +++ b/plugins/metalink/utils.c @@ -26,35 +26,6 @@ #define ATTR_LOCATION (xmlChar*)"location" #define ATTR_PREFERENCE (xmlChar*)"preference" -typedef struct _hash_op { - char *hash_type; - unsigned int length; -} hash_op; - -static hash_op hash_ops[TDNF_HASH_SENTINEL] = - { - [TDNF_HASH_MD5] = {"md5", MD5_DIGEST_LENGTH}, - [TDNF_HASH_SHA1] = {"sha1", SHA_DIGEST_LENGTH}, - [TDNF_HASH_SHA256] = {"sha256", SHA256_DIGEST_LENGTH}, - [TDNF_HASH_SHA512] = {"sha512", SHA512_DIGEST_LENGTH}, - }; - -typedef struct _hash_type { - char *hash_name; - unsigned int hash_value; -}hash_type; - -static hash_type hashType[] = - { - {"md5", TDNF_HASH_MD5}, - {"sha1", TDNF_HASH_SHA1}, - {"sha-1", TDNF_HASH_SHA1}, - {"sha256", TDNF_HASH_SHA256}, - {"sha-256", TDNF_HASH_SHA256}, - {"sha512", TDNF_HASH_SHA512}, - {"sha-512", TDNF_HASH_SHA512} - }; - static int hashTypeComparator(const void * p1, const void * p2) { return strcmp(*((const char **)p1), *((const char **)p2)); @@ -106,263 +77,6 @@ TDNFGetResourceType( goto cleanup; } -uint32_t -TDNFGetDigestForFile( - const char *filename, - hash_op *hash, - uint8_t *digest - ) -{ - uint32_t dwError = 0; - int fd = -1; - char buf[BUFSIZ] = {0}; - int length = 0; - EVP_MD_CTX *ctx = NULL; - const EVP_MD *digest_type = NULL; - unsigned int digest_length = 0; - - if (IsNullOrEmptyString(filename) || !hash || !digest) - { - dwError = ERROR_TDNF_INVALID_PARAMETER; - BAIL_ON_TDNF_ERROR(dwError); - } - - fd = open(filename, O_RDONLY); - if (fd < 0) - { - pr_err("Metalink: validating (%s) FAILED\n", filename); - dwError = errno; - BAIL_ON_TDNF_SYSTEM_ERROR_UNCOND(dwError); - } - - digest_type = EVP_get_digestbyname(hash->hash_type); - - if (!digest_type) - { - pr_err("Unknown message digest %s\n", hash->hash_type); - dwError = ERROR_TDNF_INVALID_PARAMETER; - BAIL_ON_TDNF_ERROR(dwError); - } - - ctx = EVP_MD_CTX_create(); - if (!ctx) - { - pr_err("Context Create Failed\n"); - dwError = ERROR_TDNF_CHECKSUM_VALIDATION_FAILED; - BAIL_ON_TDNF_ERROR(dwError); - } - - dwError = EVP_DigestInit_ex(ctx, digest_type, NULL); - if (!dwError) - { - pr_err("Digest Init Failed\n"); - dwError = ERROR_TDNF_CHECKSUM_VALIDATION_FAILED; - /* MD5 is not approved in FIPS mode. So, overrriding - the dwError to show the right error to the user */ -#if defined(OPENSSL_VERSION_MAJOR) && (OPENSSL_VERSION_MAJOR >= 3) - if (EVP_default_properties_is_fips_enabled(NULL) && !strcasecmp(hash->hash_type, "md5")) -#else - if (FIPS_mode() && !strcasecmp(hash->hash_type, "md5")) -#endif - { - dwError = ERROR_TDNF_FIPS_MODE_FORBIDDEN; - } - BAIL_ON_TDNF_ERROR(dwError); - } - - while ((length = read(fd, buf, (sizeof(buf)-1))) > 0) - { - dwError = EVP_DigestUpdate(ctx, buf, length); - if (!dwError) - { - pr_err("Digest Update Failed\n"); - dwError = ERROR_TDNF_CHECKSUM_VALIDATION_FAILED; - BAIL_ON_TDNF_ERROR(dwError); - } - memset(buf, 0, BUFSIZ); - } - - if (length == -1) - { - pr_err("Metalink: validating (%s) FAILED\n", filename); - dwError = errno; - BAIL_ON_TDNF_SYSTEM_ERROR(dwError); - } - - dwError = EVP_DigestFinal_ex(ctx, digest, &digest_length); - if (!dwError) - { - pr_err("Digest Final Failed\n"); - dwError = ERROR_TDNF_CHECKSUM_VALIDATION_FAILED; - BAIL_ON_TDNF_ERROR(dwError); - } - dwError = 0; - -cleanup: - if (fd >= 0) - { - close(fd); - } - if (ctx) - { - EVP_MD_CTX_destroy(ctx); - } - return dwError; -error: - goto cleanup; -} - -uint32_t -TDNFCheckHash( - const char *filename, - unsigned char *digest, - int type - ) -{ - - uint32_t dwError = 0; - uint8_t digest_from_file[EVP_MAX_MD_SIZE] = {0}; - hash_op *hash = NULL; - - if (IsNullOrEmptyString(filename) || - !digest) - { - dwError = ERROR_TDNF_INVALID_PARAMETER; - BAIL_ON_TDNF_ERROR(dwError); - } - - if (type < TDNF_HASH_MD5 || type >= TDNF_HASH_SENTINEL) - { - dwError = ERROR_TDNF_INVALID_PARAMETER; - BAIL_ON_TDNF_ERROR(dwError); - } - - hash = hash_ops + type; - - dwError = TDNFGetDigestForFile(filename, hash, digest_from_file); - BAIL_ON_TDNF_ERROR(dwError); - - if (memcmp(digest_from_file, digest, hash->length)) - { - dwError = ERROR_TDNF_CHECKSUM_VALIDATION_FAILED; - BAIL_ON_TDNF_ERROR(dwError); - } - -cleanup: - return dwError; -error: - if (!IsNullOrEmptyString(filename)) - { - pr_err("Error: Validating metalink (%s) FAILED (digest mismatch)\n", filename); - } - goto cleanup; -} - -/* Returns nonzero if hex_digest is properly formatted; that is each - letter is in [0-9A-Za-z] and the length of the string equals to the - result length of digest * 2. */ -static -uint32_t -TDNFCheckHexDigest( - const char *hex_digest, - int digest_length - ) -{ - int i = 0; - if(IsNullOrEmptyString(hex_digest) || - (digest_length <= 0)) - { - return 0; - } - for(i = 0; hex_digest[i]; ++i) - { - if(!isxdigit(hex_digest[i])) - { - return 0; - } - } - return digest_length * 2 == i; -} - -static -uint32_t -TDNFHexToUint( - const char *hex_digest, - unsigned char *uintValue - ) -{ - uint32_t dwError = 0; - char buf[3] = {0}; - unsigned long val = 0; - - if(IsNullOrEmptyString(hex_digest) || - !uintValue) - { - dwError = ERROR_TDNF_INVALID_PARAMETER; - BAIL_ON_TDNF_ERROR(dwError); - } - - buf[0] = hex_digest[0]; - buf[1] = hex_digest[1]; - - errno = 0; - val = strtoul(buf, NULL, 16); - if(errno) - { - pr_err("Error: strtoul call failed\n"); - dwError = errno; - BAIL_ON_TDNF_SYSTEM_ERROR(dwError); - } - *uintValue = (unsigned char)(val&0xff); - -cleanup: - return dwError; -error: - goto cleanup; -} - -static -uint32_t -TDNFChecksumFromHexDigest( - const char *hex_digest, - unsigned char *ppdigest - ) -{ - uint32_t dwError = 0; - unsigned char *pdigest = NULL; - size_t i = 0; - size_t len = 0; - unsigned char uintValue = 0; - - if(IsNullOrEmptyString(hex_digest) || - !ppdigest) - { - dwError = ERROR_TDNF_INVALID_PARAMETER; - BAIL_ON_TDNF_ERROR(dwError); - } - - len = strlen(hex_digest); - - dwError = TDNFAllocateMemory(1, len/2, (void **)&pdigest); - BAIL_ON_TDNF_ERROR(dwError); - - for(i = 0; i < len; i += 2) - { - dwError = TDNFHexToUint(hex_digest + i, &uintValue); - BAIL_ON_TDNF_ERROR(dwError); - - pdigest[i>>1] = uintValue; - } - memcpy( ppdigest, pdigest, len>>1 ); - -cleanup: - TDNF_SAFE_FREE_MEMORY(pdigest); - return dwError; - -error: - goto cleanup; -} - uint32_t TDNFCheckRepoMDFileHashFromMetalink( const char *pszFile, diff --git a/pytests/repo/setup-repo.sh b/pytests/repo/setup-repo.sh index 1a1bc90b..a19bd20b 100755 --- a/pytests/repo/setup-repo.sh +++ b/pytests/repo/setup-repo.sh @@ -46,6 +46,7 @@ export GNUPGHOME=${TEST_REPO_DIR}/gnupg BUILD_PATH=${TEST_REPO_DIR}/build PUBLISH_PATH=${TEST_REPO_DIR}/photon-test PUBLISH_SRC_PATH=${TEST_REPO_DIR}/photon-test-src +PUBLISH_SHA512_PATH=${TEST_REPO_DIR}/photon-test-sha512 ARCH=$(uname -m) @@ -57,6 +58,7 @@ mkdir -p -m 755 ${BUILD_PATH}/BUILD \ ${TEST_REPO_DIR}/yum.repos.d \ ${PUBLISH_PATH} \ ${PUBLISH_SRC_PATH} \ + ${PUBLISH_SHA512_PATH} \ ${GNUPGHOME} #gpgkey data for unattended key generation @@ -99,6 +101,7 @@ rpmsign --addsign ${BUILD_PATH}/RPMS/*/*.rpm check_err "Failed to sign built packages." cp -r ${BUILD_PATH}/RPMS ${PUBLISH_PATH} cp -r ${BUILD_PATH}/SRPMS ${PUBLISH_SRC_PATH} +cp -r ${BUILD_PATH}/RPMS ${PUBLISH_SHA512_PATH} # save key to later be imported: mkdir -p ${PUBLISH_PATH}/keys @@ -106,6 +109,7 @@ gpg --armor --export tdnftest@tdnf.test > ${PUBLISH_PATH}/keys/pubkey.asc createrepo ${PUBLISH_PATH} createrepo ${PUBLISH_SRC_PATH} +createrepo -s sha512 ${PUBLISH_SHA512_PATH} modifyrepo ${REPO_SRC_DIR}/updateinfo-1.xml ${PUBLISH_PATH}/repodata check_err "Failed to modify repo with updateinfo-1.xml." @@ -128,6 +132,16 @@ gpgcheck=0 enabled=1 EOF +cat << EOF > ${TEST_REPO_DIR}/yum.repos.d/photon-test-sha512.repo +[photon-test-sha512] +name=basic +baseurl=http://localhost:8080/photon-test-sha512 +#metalink=http://localhost:8080/photon-test-sha512/metalink +gpgkey=file:///etc/pki/rpm-gpg/VMWARE-RPM-GPG-KEY +gpgcheck=0 +enabled=0 +EOF + cat << EOF > ${TEST_REPO_DIR}/yum.repos.d/photon-test-src.repo [photon-test-src] name=basic diff --git a/pytests/tests/test_checksum.py b/pytests/tests/test_checksum.py new file mode 100644 index 00000000..f65cafc5 --- /dev/null +++ b/pytests/tests/test_checksum.py @@ -0,0 +1,88 @@ +# Copyright (C) 2019 - 2022 VMware, Inc. All Rights Reserved. +# +# Licensed under the GNU General Public License v2 (the "License"); +# you may not use this file except in compliance with the License. The terms +# of the License are located in the COPYING file of this distribution. +# + +import os +import shutil +import filecmp +import glob +import pytest + +WORKDIR = '/root/repofrompath/workdir' + + +@pytest.fixture(scope='module', autouse=True) +def setup_test(utils): + yield + teardown_test(utils) + + +def teardown_test(utils): + utils.run(['tdnf', 'erase', '-y', 'tdnf-test-one']) + if os.path.isdir(WORKDIR): + shutil.rmtree(WORKDIR) + + +def enable_and_create_repo(utils): + workdir = WORKDIR + utils.makedirs(workdir) + reponame = 'photon-test-sha512' + + ret = utils.run(['tdnf', '-v', '--disablerepo=*', '--enablerepo=photon-test-sha512', 'makecache']) + assert ret['retval'] == 0 + + ret = utils.run(['tdnf', '--repo={}'.format(reponame), + '--download-metadata', + 'reposync'], + cwd=workdir) + assert ret['retval'] == 0 + synced_dir = os.path.join(workdir, reponame) + assert os.path.isdir(synced_dir) + assert os.path.isdir(os.path.join(synced_dir, 'repodata')) + assert os.path.isfile(os.path.join(synced_dir, 'repodata', 'repomd.xml')) + + +def copy_rpm(workdir, orig_pkg, copy_pkg): + + orig_path = glob.glob(f"{workdir}/{orig_pkg}*.rpm") + copy_path = glob.glob(f"{workdir}/{copy_pkg}*.rpm") + + if not filecmp.cmp(copy_path[0], orig_path[0]): + shutil.copy2(copy_path[0], orig_path[0]) + + +# install package with SHA512 +def test_install_package_with_sha512_checksum(utils): + ret = utils.run(['tdnf', 'erase', '-y', 'tdnf-test-one']) + assert ret['retval'] == 0 + + ret = utils.run(['tdnf', '-y', '--nogpgcheck', 'install', '-y', 'tdnf-test-one', '--enablerepo=photon-test-sha512']) + assert ret['retval'] == 0 + + +# install package with incorrect SHA512 +def test_install_package_with_incorrect_sha512_checksum(utils): + workdir = WORKDIR + reponame = 'photon-test-sha512' + synced_dir = os.path.join(workdir, reponame) + rpm_dir = os.path.join(synced_dir, 'RPMS') + rpm_dir = os.path.join(rpm_dir, 'x86_64') + + enable_and_create_repo(utils) + copy_rpm(rpm_dir, 'tdnf-test-one', 'tdnf-test-two') + + ret = utils.run(['tdnf', + '--repofrompath=synced-repo,{}'.format(synced_dir), + '--repo=synced-repo', + 'makecache'], + cwd=workdir) + assert ret['retval'] == 0 + + ret = utils.run(['tdnf', 'erase', '-y', 'tdnf-test-one'], cwd=workdir) + assert ret['retval'] == 0 + + ret = utils.run(['tdnf', '-y', '--nogpgcheck', '--repofrompath=synced-repo,{}'.format(synced_dir), '--repo=synced-repo', 'install', '-y', 'tdnf-test-one'], cwd=workdir) + assert ret['retval'] == 1528 diff --git a/pytests/tests/test_metalink.py b/pytests/tests/test_metalink.py index 27c89054..d915065e 100644 --- a/pytests/tests/test_metalink.py +++ b/pytests/tests/test_metalink.py @@ -321,7 +321,7 @@ def test_invalid_md5_digest(utils): set_sha512(utils, False) set_invalid_md5(utils) ret = utils.run(['tdnf', 'makecache']) - assert ret['stderr'][0].startswith('Error: Validating metalink') + assert ret['stderr'][0].startswith('Error: Validating Checksum') cache_dir = utils.tdnf_config.get('main', 'cachedir') tmp_dir = os.path.join(cache_dir, 'photon-test/tmp') assert not os.path.isdir(tmp_dir) @@ -336,7 +336,7 @@ def test_invalid_sha1_digest(utils): set_sha512(utils, False) set_invalid_sha1(utils) ret = utils.run(['tdnf', 'makecache']) - assert ret['stderr'][0].startswith('Error: Validating metalink') + assert ret['stderr'][0].startswith('Error: Validating Checksum') cache_dir = utils.tdnf_config.get('main', 'cachedir') tmp_dir = os.path.join(cache_dir, 'photon-test/tmp') assert not os.path.isdir(tmp_dir) @@ -351,7 +351,7 @@ def test_invalid_sha256_digest(utils): set_sha512(utils, False) set_invalid_sha256(utils) ret = utils.run(['tdnf', 'makecache']) - assert ret['stderr'][0].startswith('Error: Validating metalink') + assert ret['stderr'][0].startswith('Error: Validating Checksum') cache_dir = utils.tdnf_config.get('main', 'cachedir') tmp_dir = os.path.join(cache_dir, 'photon-test/tmp') assert not os.path.isdir(tmp_dir) diff --git a/solv/prototypes.h b/solv/prototypes.h index a7b2d46c..2633b5e9 100644 --- a/solv/prototypes.h +++ b/solv/prototypes.h @@ -187,6 +187,13 @@ SolvGetPackageId( Id* dwPkgId ); +uint32_t +SolvGetPkgChecksumFromId( + PSolvSack pSack, + uint32_t dwPkgId, + int *checksumType, + unsigned char** ppbChecksum + ); uint32_t SolvGetLatest( diff --git a/solv/tdnfpackage.c b/solv/tdnfpackage.c index cfa66e14..3ad0d237 100644 --- a/solv/tdnfpackage.c +++ b/solv/tdnfpackage.c @@ -820,6 +820,62 @@ SolvGetPackageId( goto cleanup; } +uint32_t +SolvGetPkgChecksumFromId( + PSolvSack pSack, + uint32_t dwPkgId, + int *checksumType, + unsigned char** ppbChecksum) +{ + + uint32_t dwError = 0; + const unsigned char* pbTemp = NULL; + unsigned char* pbChecksum = NULL; + Solvable *pSolv = NULL; + int checksumLen = 0; + + if(!pSack || !pSack->pPool || !ppbChecksum) + { + dwError = ERROR_TDNF_INVALID_PARAMETER; + BAIL_ON_TDNF_ERROR(dwError); + } + + pSolv = pool_id2solvable(pSack->pPool, dwPkgId); + if(!pSolv) + { + dwError = ERROR_TDNF_NO_DATA; + BAIL_ON_TDNF_ERROR(dwError); + } + + pbTemp = solvable_lookup_bin_checksum(pSolv, SOLVABLE_CHECKSUM, checksumType); + + if(!pbTemp) + { + dwError = ERROR_TDNF_NO_DATA; + BAIL_ON_TDNF_ERROR(dwError); + } + + checksumLen = solv_chksum_len(*checksumType); + + dwError = TDNFAllocateMemory(checksumLen, sizeof(unsigned char), (void **)&pbChecksum); + BAIL_ON_TDNF_ERROR(dwError); + + memcpy(pbChecksum, pbTemp, checksumLen); + *ppbChecksum = pbChecksum; + +cleanup: + return dwError; + +error: + if(ppbChecksum) + { + *ppbChecksum = NULL; + } + TDNF_SAFE_FREE_MEMORY(pbChecksum); + goto cleanup; + +} + uint32_t SolvCmpEvr( PSolvSack pSack, From 140c2a42589f9dc5f8224ece84cab656be9d4a23 Mon Sep 17 00:00:00 2001 From: Oliver Kurth Date: Wed, 2 Aug 2023 11:20:59 -0700 Subject: [PATCH 3/5] Merge pull request #443 from oliverkurth/topic/okurth/minor-annoyances Minor fixes --- client/remoterepo.c | 9 +++++++-- pytests/tests/test_json.py | 10 +++++----- solv/tdnfquery.c | 1 + tools/cli/lib/api.c | 4 ++-- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/client/remoterepo.c b/client/remoterepo.c index 60adbb03..fad15b5b 100644 --- a/client/remoterepo.c +++ b/client/remoterepo.c @@ -163,6 +163,7 @@ TDNFDownloadFile( /* lStatus reads CURLINFO_RESPONSE_CODE. Must be long */ long lStatus = 0; int i; + int nNoOutput = 1; /* TDNFFetchRemoteGPGKey sends pszProgressData as NULL */ if(!pTdnf || @@ -215,6 +216,7 @@ TDNFDownloadFile( { dwError = set_progress_cb(pCurl, pszProgressData); BAIL_ON_TDNF_ERROR(dwError); + nNoOutput = 0; } } @@ -253,6 +255,11 @@ TDNFDownloadFile( fclose(fp); fp = NULL; } + /* finish progress line output, + but only if progrees was enabled */ + if (!nNoOutput) { + pr_info("\n"); + } dwError = curl_easy_getinfo(pCurl, CURLINFO_RESPONSE_CODE, @@ -405,8 +412,6 @@ TDNFDownloadPackage( } BAIL_ON_TDNF_ERROR(dwError); - pr_info("\n"); - cleanup: TDNF_SAFE_FREE_MEMORY(pszCopyOfPackageLocation); TDNF_SAFE_FREE_MEMORY(pszPackageFile); diff --git a/pytests/tests/test_json.py b/pytests/tests/test_json.py index 553736a3..8134e6cd 100644 --- a/pytests/tests/test_json.py +++ b/pytests/tests/test_json.py @@ -141,7 +141,7 @@ def test_erase_verbose(utils): def test_check_update(utils): ret = utils.run(['tdnf', '-j', 'check-update']) d = json.loads("\n".join(ret['stdout'])) - assert type(d) == list + assert type(d) is list def test_repolist(utils): @@ -160,25 +160,25 @@ def test_repolist(utils): def test_repoquery(utils): ret = utils.run(['tdnf', '-j', 'repoquery']) d = json.loads("\n".join(ret['stdout'])) - assert type(d) == list + assert type(d) is list def test_updateinfo(utils): ret = utils.run(['tdnf', '-j', 'updateinfo']) d = json.loads("\n".join(ret['stdout'])) - assert type(d) == dict + assert type(d) is dict def test_updateinfo_info(utils): ret = utils.run(['tdnf', '-j', 'updateinfo', '--info']) d = json.loads("\n".join(ret['stdout'])) - assert type(d) == list + assert type(d) is list def test_history_info(utils): ret = utils.run(['tdnf', '-j', 'history', '--info']) d = json.loads("\n".join(ret['stdout'])) - assert type(d) == list + assert type(d) is list def test_jsondump(utils): diff --git a/solv/tdnfquery.c b/solv/tdnfquery.c index e5d9144b..78b83f47 100644 --- a/solv/tdnfquery.c +++ b/solv/tdnfquery.c @@ -606,6 +606,7 @@ SolvApplyListQuery( SELECTION_PROVIDES | SELECTION_GLOB | /* foo* */ SELECTION_CANON | /* foo-1.2-3.ph4.noarch */ + SELECTION_DOTARCH | /* foo.noarch */ SELECTION_REL; /* foo>=1.2-3 */ if (pQuery->nScope == SCOPE_SOURCE) { diff --git a/tools/cli/lib/api.c b/tools/cli/lib/api.c index c58c8909..f55974b9 100644 --- a/tools/cli/lib/api.c +++ b/tools/cli/lib/api.c @@ -174,7 +174,7 @@ TDNFCliListPackagesPrint( if(snprintf( szNameAndArch, MAX_COL_LEN, - "%s.%s", + "%s.%s ", pPkg->pszName, pPkg->pszArch) < 0) { @@ -186,7 +186,7 @@ TDNFCliListPackagesPrint( if(snprintf( szVersionAndRelease, MAX_COL_LEN, - "%s-%s", + "%s-%s ", pPkg->pszVersion, pPkg->pszRelease) < 0) { From 3a7a05e3e7edee534277330ac8c0c87a8cfc313d Mon Sep 17 00:00:00 2001 From: Oliver Kurth Date: Thu, 3 Aug 2023 13:46:33 -0700 Subject: [PATCH 4/5] Merge pull request #445 from sshedi/lock-fix don't try to remove lockfile when running as regular user --- client/api.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/api.c b/client/api.c index 8146f018..65895c7c 100644 --- a/client/api.c +++ b/client/api.c @@ -31,6 +31,11 @@ static void IsTdnfAlreadyRunning(void); static void TdnfExitHandler(void) { + if (gEuid) + { + return; + } + tdnflockFree(instance_lock); } From 27b952b44902f38e43ce5740584c6d840718566f Mon Sep 17 00:00:00 2001 From: Oliver Kurth Date: Mon, 21 Aug 2023 12:00:59 -0700 Subject: [PATCH 5/5] bump version to 3.5.5 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a39fbf03..a02e0070 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ CMAKE_MINIMUM_REQUIRED(VERSION 3.0 FATAL_ERROR) -project(tdnf VERSION 3.5.4 LANGUAGES C) +project(tdnf VERSION 3.5.5 LANGUAGES C) set(VERSION ${PROJECT_VERSION}) set(PROJECT_YEAR 2023)