Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

work towards enforcing availability of SHA256 checksums for all sources/patches #2215

Merged
merged 9 commits into from
May 19, 2017
20 changes: 16 additions & 4 deletions easybuild/framework/easyblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
from easybuild.tools.config import build_option, build_path, get_log_filename, get_repository, get_repositorypath
from easybuild.tools.config import install_path, log_path, package_path, source_paths
from easybuild.tools.environment import restore_env, sanitize_env
from easybuild.tools.filetools import DEFAULT_CHECKSUM
from easybuild.tools.filetools import CHECKSUM_TYPE_MD5, CHECKSUM_TYPE_SHA256
from easybuild.tools.filetools import adjust_permissions, apply_patch, change_dir, convert_name, compute_checksum
from easybuild.tools.filetools import copy_file, derive_alt_pypi_url, download_file, encode_class_name, extract_file
from easybuild.tools.filetools import is_alt_pypi_url, mkdir, move_logs, read_file, remove_file, rmtree2, write_file
Expand Down Expand Up @@ -471,6 +471,11 @@ def fetch_extension_sources(self):
if src_fn:
ext_src.update({'src': src_fn})

# report both MD5 and SHA256 checksums, since both are valid default checksum types
for checksum_type in (CHECKSUM_TYPE_MD5, CHECKSUM_TYPE_SHA256):
src_checksum = compute_checksum(src_fn, checksum_type=checksum_type)
self.log.info("%s checksum for %s: %s", checksum_type, src_fn, src_checksum)

if checksums:
fn_checksum = self.get_checksum_for(checksums, filename=src_fn, index=0)
if verify_checksum(src_fn, fn_checksum):
Expand All @@ -483,6 +488,12 @@ def fetch_extension_sources(self):
self.log.debug('Found patches for extension %s: %s' % (ext_name, ext_patches))
ext_src.update({'patches': ext_patches})

for patch in ext_patches:
# report both MD5 and SHA256 checksums, since both are valid default checksum types
for checksum_type in (CHECKSUM_TYPE_MD5, CHECKSUM_TYPE_SHA256):
patch_checksum = compute_checksum(patch, checksum_type=checksum_type)
self.log.info("%s checksum for %s: %s", checksum_type, patch, patch_checksum)

if checksums:
self.log.debug('Verifying checksums for extension patches...')
for index, ext_patch in enumerate(ext_patches):
Expand Down Expand Up @@ -1508,9 +1519,10 @@ def fetch_step(self, skip_checksums=False):
# compute checksums for all source and patch files
if not (skip_checksums or self.dry_run):
for fil in self.src + self.patches:
check_sum = compute_checksum(fil['path'], checksum_type=DEFAULT_CHECKSUM)
fil[DEFAULT_CHECKSUM] = check_sum
self.log.info("%s checksum for %s: %s" % (DEFAULT_CHECKSUM, fil['path'], fil[DEFAULT_CHECKSUM]))
# report both MD5 and SHA256 checksums, since both are valid default checksum types
for checksum_type in [CHECKSUM_TYPE_MD5, CHECKSUM_TYPE_SHA256]:
fil[checksum_type] = compute_checksum(fil['path'], checksum_type=checksum_type)
self.log.info("%s checksum for %s: %s", checksum_type, fil['path'], fil[checksum_type])

# fetch extensions
if self.cfg['exts_list']:
Expand Down
1 change: 1 addition & 0 deletions easybuild/tools/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
'debug',
'debug_lmod',
'dump_autopep8',
'enforce_checksums',
'extended_dry_run',
'experimental',
'fixed_installdir_naming_scheme',
Expand Down
30 changes: 21 additions & 9 deletions easybuild/tools/filetools.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,18 @@
}


# default checksum for source and patch files
DEFAULT_CHECKSUM = 'md5'
CHECKSUM_TYPE_MD5 = 'md5'
CHECKSUM_TYPE_SHA256 = 'sha256'
DEFAULT_CHECKSUM = CHECKSUM_TYPE_MD5

# map of checksum types to checksum functions
CHECKSUM_FUNCTIONS = {
'md5': lambda p: calc_block_checksum(p, hashlib.md5()),
'sha1': lambda p: calc_block_checksum(p, hashlib.sha1()),
'adler32': lambda p: calc_block_checksum(p, ZlibChecksum(zlib.adler32)),
'crc32': lambda p: calc_block_checksum(p, ZlibChecksum(zlib.crc32)),
CHECKSUM_TYPE_MD5: lambda p: calc_block_checksum(p, hashlib.md5()),
'sha1': lambda p: calc_block_checksum(p, hashlib.sha1()),
CHECKSUM_TYPE_SHA256: lambda p: calc_block_checksum(p, hashlib.sha256()),
'sha512': lambda p: calc_block_checksum(p, hashlib.sha512()),
'size': lambda p: os.path.getsize(p),
}

Expand Down Expand Up @@ -551,7 +554,7 @@ def compute_checksum(path, checksum_type=DEFAULT_CHECKSUM):
Compute checksum of specified file.

:param path: Path of file to compute checksum for
:param checksum_type: Type of checksum ('adler32', 'crc32', 'md5' (default), 'sha1', 'size')
:param checksum_type: type(s) of checksum ('adler32', 'crc32', 'md5' (default), 'sha1', 'sha256', 'sha512', 'size')
"""
if checksum_type not in CHECKSUM_FUNCTIONS:
raise EasyBuildError("Unknown checksum type (%s), supported types are: %s",
Expand Down Expand Up @@ -597,18 +600,27 @@ def verify_checksum(path, checksums):
:param file: path of file to verify checksum of
:param checksum: checksum value (and type, optionally, default is MD5), e.g., 'af314', ('sha', '5ec1b')
"""
# if no checksum is provided, pretend checksum to be valid
# if no checksum is provided, pretend checksum to be valid, unless presence of checksums to verify is enforced
if checksums is None:
return True
if build_option('enforce_checksums'):
raise EasyBuildError("Missing checksum for %s", os.path.basename(path))
else:
return True

# make sure we have a list of checksums
if not isinstance(checksums, list):
checksums = [checksums]

for checksum in checksums:
if isinstance(checksum, basestring):
# default checksum type unless otherwise specified is MD5 (most common(?))
typ = DEFAULT_CHECKSUM
# if no checksum type is specified, it is assumed to be MD5 (32 characters) or SHA256 (64 characters)
if len(checksum) == 64:
typ = CHECKSUM_TYPE_SHA256
elif len(checksum) == 32:
typ = CHECKSUM_TYPE_MD5
else:
raise EasyBuildError("Length of checksum '%s' (%d) does not match with either MD5 (32) or SHA256 (64)",
checksum, len(checksum))
elif isinstance(checksum, tuple) and len(checksum) == 2:
typ, checksum = checksum
else:
Expand Down
2 changes: 2 additions & 0 deletions easybuild/tools/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,8 @@ def override_options(self):
'dump-autopep8': ("Reformat easyconfigs using autopep8 when dumping them", None, 'store_true', False),
'easyblock': ("easyblock to use for processing the spec file or dumping the options",
None, 'store', None, 'e', {'metavar': 'CLASS'}),
'enforce-checksums': ("Enforce availability of checksums for all sources/patches, so they can be verified",
None, 'store_true', False),
'experimental': ("Allow experimental code (with behaviour that can be changed/removed at any given time).",
None, 'store_true', False),
'extra-modules': ("List of extra modules to load after setting up the build environment",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ patches = ['toy-0.0_typo.patch']
exts_list = [
('bar', '0.0', {
'buildopts': " && gcc bar.c -o anotherbar",
'patches': ['bar-0.0_typo.patch'],
'toy_ext_param': "mv anotherbar bar_bis",
'unknowneasyconfigparameterthatshouldbeignored': 'foo',
}),
Expand Down
30 changes: 28 additions & 2 deletions test/framework/filetools.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,24 +184,50 @@ def test_checksums(self):
'crc32': '0x1457143216',
'md5': '7167b64b1ca062b9674ffef46f9325db',
'sha1': 'db05b79e09a4cc67e9dd30b313b5488813db3190',
'sha256': '1c49562c4b404f3120a3fa0926c8d09c99ef80e470f7de03ffdfa14047960ea5',
'sha512': '7610f6ce5e91e56e350d25c917490e4815f7986469fafa41056698aec256733eb7297da8b547d5e74b851d7c4e475900cec4744df0f887ae5c05bf1757c224b4',
}

# make sure checksums computation/verification is correct
for checksum_type, checksum in known_checksums.items():
self.assertEqual(ft.compute_checksum(fp, checksum_type=checksum_type), checksum)
self.assertTrue(ft.verify_checksum(fp, (checksum_type, checksum)))
# md5 is default

# default checksum type is MD5
self.assertEqual(ft.compute_checksum(fp), known_checksums['md5'])

# both MD5 and SHA256 checksums can be verified without specifying type
self.assertTrue(ft.verify_checksum(fp, known_checksums['md5']))
self.assertTrue(ft.verify_checksum(fp, known_checksums['sha256']))

# checksum of length 32 is assumed to be MD5, length 64 to be SHA256, other lengths not allowed
# providing non-matching MD5 and SHA256 checksums results in failed verification
self.assertFalse(ft.verify_checksum(fp, '1c49562c4b404f3120a3fa0926c8d09c'))
self.assertFalse(ft.verify_checksum(fp, '7167b64b1ca062b9674ffef46f9325db7167b64b1ca062b9674ffef46f9325db'))
# checksum of length other than 32/64 yields an error
error_pattern = "Length of checksum '.*' \(\d+\) does not match with either MD5 \(32\) or SHA256 \(64\)"
for checksum in ['tooshort', 'inbetween32and64charactersisnotgoodeither', known_checksums['sha256'] + 'foo']:
self.assertErrorRegex(EasyBuildError, error_pattern, ft.verify_checksum, fp, checksum)

# make sure faulty checksums are reported
broken_checksums = dict([(typ, val + 'foo') for (typ, val) in known_checksums.items()])
broken_checksums = dict([(typ, val[:-3] + 'foo') for (typ, val) in known_checksums.items()])
for checksum_type, checksum in broken_checksums.items():
self.assertFalse(ft.compute_checksum(fp, checksum_type=checksum_type) == checksum)
self.assertFalse(ft.verify_checksum(fp, (checksum_type, checksum)))
# md5 is default
self.assertFalse(ft.compute_checksum(fp) == broken_checksums['md5'])
self.assertFalse(ft.verify_checksum(fp, broken_checksums['md5']))
self.assertFalse(ft.verify_checksum(fp, broken_checksums['sha256']))

# check whether missing checksums are enforced
build_options = {
'enforce_checksums': True,
}
init_config(build_options=build_options)

self.assertErrorRegex(EasyBuildError, "Missing checksum for", ft.verify_checksum, fp, None)
self.assertTrue(ft.verify_checksum(fp, known_checksums['md5']))
self.assertTrue(ft.verify_checksum(fp, known_checksums['sha256']))

# cleanup
os.remove(fp)
Expand Down
12 changes: 12 additions & 0 deletions test/framework/sandbox/sources/toy/extensions/bar-0.0_typo.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
diff --git a/bar-0.0/bar.source.orig b/bar-0.0/bar.source
index 0e0c6a74..36880e71 100644
--- a/bar-0.0/bar.source.orig
+++ b/bar-0.0/bar.source
@@ -2,6 +2,6 @@

int main(int argc, char* argv[]){

- printf("I'm a toy, and proud of it.\n");
+ printf("I'm a bar, and very proud of it.\n");
return 0;
}
2 changes: 1 addition & 1 deletion test/framework/toy_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ def test_toy_broken(self):
broken_toy_ec = os.path.join(tmpdir, "toy-broken.eb")
toy_ec_file = os.path.join(os.path.dirname(__file__), 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')
broken_toy_ec_txt = read_file(toy_ec_file)
broken_toy_ec_txt += "checksums = ['clearywrongchecksum']"
broken_toy_ec_txt += "checksums = ['clearywrongMD5checksumoflength32']"
write_file(broken_toy_ec, broken_toy_ec_txt)
error_regex = "Checksum verification .* failed"
self.assertErrorRegex(EasyBuildError, error_regex, self.test_toy_build, ec_file=broken_toy_ec, tmpdir=tmpdir,
Expand Down