From d2f653e816b7e8118e8753a8caceb858d8a61620 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 13 Jun 2023 20:17:01 +0200 Subject: [PATCH] check: rebuild_manifest must verify archive TAM --- src/borg/archive.py | 13 ++++++++ src/borg/crypto/key.py | 56 ++++++++++++++++++++++++++++++++++ src/borg/helpers.py | 6 ++++ src/borg/testsuite/archiver.py | 5 +-- 4 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 7d9bfc286..ad376d548 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1435,6 +1435,19 @@ class ArchiveChecker: except (TypeError, ValueError, StopIteration): continue if valid_archive(archive): + # **after** doing the low-level checks and having a strong indication that we + # are likely looking at an archive item here, also check the TAM authentication: + try: + archive, verified = self.key.unpack_and_verify_archive(data, force_tam_not_required=False) + except IntegrityError: + # TAM issues - do not accept this archive! + # either somebody is trying to attack us with a fake archive data or + # we have an ancient archive made before TAM was a thing (borg < 1.0.9) **and** this repo + # was not correctly upgraded to borg 1.2.5 (see advisory at top of the changelog). + # borg can't tell the difference, so it has to assume this archive might be an attack + # and drops this archive. + continue + # note: if we get here and verified is False, a TAM is not required. archive = ArchiveItem(internal_dict=archive) name = archive.name logger.info('Found archive %s', name) diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index 720be0d16..782b8a5cb 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -87,6 +87,13 @@ class TAMRequiredError(IntegrityError): traceback = False +class ArchiveTAMRequiredError(TAMRequiredError): + __doc__ = textwrap.dedent(""" + Archive '{}' is unauthenticated, but it is required for this repository. + """).strip() + traceback = False + + class TAMInvalid(IntegrityError): __doc__ = IntegrityError.__doc__ traceback = False @@ -96,6 +103,15 @@ class TAMInvalid(IntegrityError): super().__init__('Manifest authentication did not verify') +class ArchiveTAMInvalid(IntegrityError): + __doc__ = IntegrityError.__doc__ + traceback = False + + def __init__(self): + # Error message becomes: "Data integrity error: Archive authentication did not verify" + super().__init__('Archive authentication did not verify') + + class TAMUnsupportedSuiteError(IntegrityError): """Could not verify manifest: Unsupported suite {!r}; a newer version is needed.""" traceback = False @@ -264,6 +280,46 @@ class KeyBase: logger.debug('TAM-verified manifest') return unpacked, True + def unpack_and_verify_archive(self, data, force_tam_not_required=False): + """Unpack msgpacked *data* and return (object, did_verify).""" + tam_required = self.tam_required + if force_tam_not_required and tam_required: + logger.warning('Archive authentication DISABLED.') + tam_required = False + data = bytearray(data) + unpacker = get_limited_unpacker('archive') + unpacker.feed(data) + unpacked = unpacker.unpack() + if b'tam' not in unpacked: + if tam_required: + archive_name = unpacked.get(b'name', b'').decode('ascii', 'replace') + raise ArchiveTAMRequiredError(archive_name) + else: + logger.debug('TAM not found and not required') + return unpacked, False + tam = unpacked.pop(b'tam', None) + if not isinstance(tam, dict): + raise ArchiveTAMInvalid() + tam_type = tam.get(b'type', b'').decode('ascii', 'replace') + if tam_type != 'HKDF_HMAC_SHA512': + if tam_required: + raise TAMUnsupportedSuiteError(repr(tam_type)) + else: + logger.debug('Ignoring TAM made with unsupported suite, since TAM is not required: %r', tam_type) + return unpacked, False + tam_hmac = tam.get(b'hmac') + tam_salt = tam.get(b'salt') + if not isinstance(tam_salt, bytes) or not isinstance(tam_hmac, bytes): + raise ArchiveTAMInvalid() + offset = data.index(tam_hmac) + data[offset:offset + 64] = bytes(64) + tam_key = self._tam_key(tam_salt, context=b'archive') + calculated_hmac = HMAC(tam_key, data, sha512).digest() + if not compare_digest(calculated_hmac, tam_hmac): + raise ArchiveTAMInvalid() + logger.debug('TAM-verified archive') + return unpacked, True + class PlaintextKey(KeyBase): TYPE = 0x02 diff --git a/src/borg/helpers.py b/src/borg/helpers.py index e1150bf00..c00563345 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -213,6 +213,12 @@ def get_limited_unpacker(kind): object_hook=StableDict, unicode_errors='surrogateescape', )) + elif kind == 'archive': + args.update(dict(use_list=True, # default value + max_map_len=100, # ARCHIVE_KEYS ~= 20 + max_str_len=10000, # comment + object_hook=StableDict, + )) elif kind == 'key': args.update(dict(use_list=True, # default value max_array_len=0, # not used diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 1fcaa69ae..624a33818 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -3417,7 +3417,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): corrupted_manifest = manifest + b'corrupted!' repository.put(Manifest.MANIFEST_ID, corrupted_manifest) - archive = msgpack.packb({ + archive_dict = { 'cmdline': [], 'items': [], 'hostname': 'foo', @@ -3425,7 +3425,8 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): 'name': 'archive1', 'time': '2016-12-15T18:49:51.849711', 'version': 1, - }) + } + archive = key.pack_and_authenticate_metadata(archive_dict, context=b'archive') archive_id = key.id_hash(archive) repository.put(archive_id, key.encrypt(archive)) repository.commit()