1
0
Fork 0
mirror of https://github.com/borgbackup/borg.git synced 2025-03-10 06:03:38 +00:00

check: rebuild_manifest must verify archive TAM

This commit is contained in:
Thomas Waldmann 2023-06-13 20:17:01 +02:00
parent 8ae06199ef
commit 1fd94bd38f
No known key found for this signature in database
GPG key ID: 243ACFA951F78E01
4 changed files with 78 additions and 2 deletions

View file

@ -1813,6 +1813,19 @@ class ArchiveChecker:
except msgpack.UnpackException: except msgpack.UnpackException:
continue continue
if valid_archive(archive): 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) archive = ArchiveItem(internal_dict=archive)
name = archive.name name = archive.name
logger.info('Found archive %s', name) logger.info('Found archive %s', name)

View file

@ -89,6 +89,13 @@ class TAMRequiredError(IntegrityError):
traceback = False traceback = False
class ArchiveTAMRequiredError(TAMRequiredError):
__doc__ = textwrap.dedent("""
Archive '{}' is unauthenticated, but it is required for this repository.
""").strip()
traceback = False
class TAMInvalid(IntegrityError): class TAMInvalid(IntegrityError):
__doc__ = IntegrityError.__doc__ __doc__ = IntegrityError.__doc__
traceback = False traceback = False
@ -98,6 +105,15 @@ class TAMInvalid(IntegrityError):
super().__init__('Manifest authentication did not verify') 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): class TAMUnsupportedSuiteError(IntegrityError):
"""Could not verify manifest: Unsupported suite {!r}; a newer version is needed.""" """Could not verify manifest: Unsupported suite {!r}; a newer version is needed."""
traceback = False traceback = False
@ -266,6 +282,46 @@ class KeyBase:
logger.debug('TAM-verified manifest') logger.debug('TAM-verified manifest')
return unpacked, True 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'<unknown>').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'<none>').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.digest(tam_key, data, 'sha512')
if not hmac.compare_digest(calculated_hmac, tam_hmac):
raise ArchiveTAMInvalid()
logger.debug('TAM-verified archive')
return unpacked, True
class PlaintextKey(KeyBase): class PlaintextKey(KeyBase):
TYPE = 0x02 TYPE = 0x02

View file

@ -209,6 +209,12 @@ def get_limited_unpacker(kind):
max_str_len=255, # archive name max_str_len=255, # archive name
object_hook=StableDict, object_hook=StableDict,
)) ))
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': elif kind == 'key':
args.update(dict(use_list=True, # default value args.update(dict(use_list=True, # default value
max_array_len=0, # not used max_array_len=0, # not used

View file

@ -3957,7 +3957,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase):
corrupted_manifest = manifest + b'corrupted!' corrupted_manifest = manifest + b'corrupted!'
repository.put(Manifest.MANIFEST_ID, corrupted_manifest) repository.put(Manifest.MANIFEST_ID, corrupted_manifest)
archive = msgpack.packb({ archive_dict = {
'cmdline': [], 'cmdline': [],
'items': [], 'items': [],
'hostname': 'foo', 'hostname': 'foo',
@ -3965,7 +3965,8 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase):
'name': 'archive1', 'name': 'archive1',
'time': '2016-12-15T18:49:51.849711', 'time': '2016-12-15T18:49:51.849711',
'version': 1, 'version': 1,
}) }
archive = key.pack_and_authenticate_metadata(archive_dict, context=b'archive')
archive_id = key.id_hash(archive) archive_id = key.id_hash(archive)
repository.put(archive_id, key.encrypt(archive)) repository.put(archive_id, key.encrypt(archive))
repository.commit(compact=False) repository.commit(compact=False)